borsapy
borsapy - Turkish Financial Markets Data Library
A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data.
Examples:
import borsapy as bp
# Get stock data >>> stock = bp.Ticker("THYAO") >>> stock.info # Real-time quote >>> stock.history(period="1mo") # OHLCV data >>> stock.balance_sheet # Financial statements # Get forex/commodity data >>> usd = bp.FX("USD") >>> usd.current # Current rate >>> usd.history(period="1mo") # Historical data >>> usd.bank_rates # Bank exchange rates >>> usd.bank_rate("akbank") # Single bank rate >>> bp.banks() # List supported banks >>> gold = bp.FX("gram-altin") # List all BIST companies >>> bp.companies() >>> bp.search_companies("banka") # Get crypto data >>> btc = bp.Crypto("BTCTRY") >>> btc.current # Current price >>> btc.history(period="1mo") # Historical OHLCV >>> bp.crypto_pairs() # List available pairs # Get fund data >>> fund = bp.Fund("AAK") >>> fund.info # Fund details >>> fund.history(period="1mo") # Price history # Get inflation data >>> inf = bp.Inflation() >>> inf.latest() # Latest TÜFE data >>> inf.calculate(100000, "2020-01", "2024-01") # Inflation calculation # Economic calendar >>> cal = bp.EconomicCalendar() >>> cal.events(period="1w") # This week's events >>> cal.today() # Today's events >>> bp.economic_calendar(country="TR", importance="high") # Government bonds >>> bp.bonds() # All bond yields >>> bond = bp.Bond("10Y") >>> bond.yield_rate # Current 10Y yield >>> bp.risk_free_rate() # For DCF calculations # Stock screener >>> bp.screen_stocks(template="high_dividend") >>> bp.screen_stocks(market_cap_min=1000, pe_max=15) >>> screener = bp.Screener() >>> screener.add_filter("dividend_yield", min=3).run() # Real-time streaming (low-latency) >>> stream = bp.TradingViewStream() >>> stream.connect() >>> stream.subscribe("THYAO") >>> quote = stream.get_quote("THYAO") # Instant (~1ms) >>> quote['last'] # Last price >>> stream.disconnect() # Context manager >>> with bp.TradingViewStream() as stream: ... stream.subscribe("THYAO") ... quote = stream.wait_for_quote("THYAO") ... print(quote['last']) # Symbol search >>> bp.search("banka") # Search all markets >>> bp.search_bist("enerji") # BIST only >>> bp.search_crypto("BTC") # Crypto only >>> bp.search("THYAO", full_info=True) # Detailed results # Heikin Ashi charts >>> df = stock.history(period="1y") >>> ha_df = bp.calculate_heikin_ashi(df) # Returns HA_Open, HA_High, HA_Low, HA_Close >>> ha_df = stock.heikin_ashi(period="1y") # Convenience method # Chart streaming (OHLCV candles via WebSocket) >>> stream = bp.TradingViewStream() >>> stream.connect() >>> stream.subscribe_chart("THYAO", "1m") # 1-minute candles >>> candle = stream.get_candle("THYAO", "1m") >>> print(candle['close']) # Historical replay for backtesting >>> session = bp.create_replay("THYAO", period="1y", speed=100) >>> for candle in session.replay(): ... print(f"{candle['timestamp']}: {candle['close']}") # Backtest engine >>> def rsi_strategy(candle, position, indicators): ... if indicators['rsi'] < 30 and position is None: ... return 'BUY' ... elif indicators['rsi'] > 70 and position == 'long': ... return 'SELL' ... return 'HOLD' >>> result = bp.backtest("THYAO", rsi_strategy, period="1y", indicators=['rsi']) >>> print(result.summary()) >>> print(f"Win Rate: {result.win_rate:.1f}%") # Pine Script streaming indicators >>> stream = bp.TradingViewStream() >>> stream.connect() >>> stream.subscribe_chart("THYAO", "1m") >>> stream.add_study("THYAO", "1m", "RSI") >>> stream.add_study("THYAO", "1m", "MACD") >>> rsi = stream.get_study("THYAO", "1m", "RSI") >>> print(rsi['value'])
1""" 2borsapy - Turkish Financial Markets Data Library 3 4A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data. 5 6Examples: 7 >>> import borsapy as bp 8 9 # Get stock data 10 >>> stock = bp.Ticker("THYAO") 11 >>> stock.info # Real-time quote 12 >>> stock.history(period="1mo") # OHLCV data 13 >>> stock.balance_sheet # Financial statements 14 15 # Get forex/commodity data 16 >>> usd = bp.FX("USD") 17 >>> usd.current # Current rate 18 >>> usd.history(period="1mo") # Historical data 19 >>> usd.bank_rates # Bank exchange rates 20 >>> usd.bank_rate("akbank") # Single bank rate 21 >>> bp.banks() # List supported banks 22 >>> gold = bp.FX("gram-altin") 23 24 # List all BIST companies 25 >>> bp.companies() 26 >>> bp.search_companies("banka") 27 28 # Get crypto data 29 >>> btc = bp.Crypto("BTCTRY") 30 >>> btc.current # Current price 31 >>> btc.history(period="1mo") # Historical OHLCV 32 >>> bp.crypto_pairs() # List available pairs 33 34 # Get fund data 35 >>> fund = bp.Fund("AAK") 36 >>> fund.info # Fund details 37 >>> fund.history(period="1mo") # Price history 38 39 # Get inflation data 40 >>> inf = bp.Inflation() 41 >>> inf.latest() # Latest TÜFE data 42 >>> inf.calculate(100000, "2020-01", "2024-01") # Inflation calculation 43 44 # Economic calendar 45 >>> cal = bp.EconomicCalendar() 46 >>> cal.events(period="1w") # This week's events 47 >>> cal.today() # Today's events 48 >>> bp.economic_calendar(country="TR", importance="high") 49 50 # Government bonds 51 >>> bp.bonds() # All bond yields 52 >>> bond = bp.Bond("10Y") 53 >>> bond.yield_rate # Current 10Y yield 54 >>> bp.risk_free_rate() # For DCF calculations 55 56 # Stock screener 57 >>> bp.screen_stocks(template="high_dividend") 58 >>> bp.screen_stocks(market_cap_min=1000, pe_max=15) 59 >>> screener = bp.Screener() 60 >>> screener.add_filter("dividend_yield", min=3).run() 61 62 # Real-time streaming (low-latency) 63 >>> stream = bp.TradingViewStream() 64 >>> stream.connect() 65 >>> stream.subscribe("THYAO") 66 >>> quote = stream.get_quote("THYAO") # Instant (~1ms) 67 >>> quote['last'] # Last price 68 >>> stream.disconnect() 69 70 # Context manager 71 >>> with bp.TradingViewStream() as stream: 72 ... stream.subscribe("THYAO") 73 ... quote = stream.wait_for_quote("THYAO") 74 ... print(quote['last']) 75 76 # Symbol search 77 >>> bp.search("banka") # Search all markets 78 >>> bp.search_bist("enerji") # BIST only 79 >>> bp.search_crypto("BTC") # Crypto only 80 >>> bp.search("THYAO", full_info=True) # Detailed results 81 82 # Heikin Ashi charts 83 >>> df = stock.history(period="1y") 84 >>> ha_df = bp.calculate_heikin_ashi(df) # Returns HA_Open, HA_High, HA_Low, HA_Close 85 >>> ha_df = stock.heikin_ashi(period="1y") # Convenience method 86 87 # Chart streaming (OHLCV candles via WebSocket) 88 >>> stream = bp.TradingViewStream() 89 >>> stream.connect() 90 >>> stream.subscribe_chart("THYAO", "1m") # 1-minute candles 91 >>> candle = stream.get_candle("THYAO", "1m") 92 >>> print(candle['close']) 93 94 # Historical replay for backtesting 95 >>> session = bp.create_replay("THYAO", period="1y", speed=100) 96 >>> for candle in session.replay(): 97 ... print(f"{candle['timestamp']}: {candle['close']}") 98 99 # Backtest engine 100 >>> def rsi_strategy(candle, position, indicators): 101 ... if indicators['rsi'] < 30 and position is None: 102 ... return 'BUY' 103 ... elif indicators['rsi'] > 70 and position == 'long': 104 ... return 'SELL' 105 ... return 'HOLD' 106 >>> result = bp.backtest("THYAO", rsi_strategy, period="1y", indicators=['rsi']) 107 >>> print(result.summary()) 108 >>> print(f"Win Rate: {result.win_rate:.1f}%") 109 110 # Pine Script streaming indicators 111 >>> stream = bp.TradingViewStream() 112 >>> stream.connect() 113 >>> stream.subscribe_chart("THYAO", "1m") 114 >>> stream.add_study("THYAO", "1m", "RSI") 115 >>> stream.add_study("THYAO", "1m", "MACD") 116 >>> rsi = stream.get_study("THYAO", "1m", "RSI") 117 >>> print(rsi['value']) 118""" 119 120# TradingView authentication for real-time data 121from borsapy._providers.tradingview import ( 122 clear_tradingview_auth, 123 get_tradingview_auth, 124 set_tradingview_auth, 125) 126from borsapy.backtest import Backtest, BacktestResult, Trade, backtest 127from borsapy.bond import Bond, bonds, risk_free_rate 128from borsapy.calendar import EconomicCalendar, economic_calendar 129from borsapy.charts import calculate_heikin_ashi 130from borsapy.crypto import Crypto, crypto_pairs 131from borsapy.eurobond import Eurobond, eurobonds 132from borsapy.exceptions import ( 133 APIError, 134 AuthenticationError, 135 BorsapyError, 136 DataNotAvailableError, 137 InvalidIntervalError, 138 InvalidPeriodError, 139 RateLimitError, 140 TickerNotFoundError, 141) 142from borsapy.fund import Fund, compare_funds, screen_funds, search_funds 143from borsapy.fx import FX, banks, metal_institutions 144from borsapy.index import Index, all_indices, index, indices 145from borsapy.inflation import Inflation 146from borsapy.market import companies, search_companies 147from borsapy.multi import Tickers, download 148from borsapy.portfolio import Portfolio 149from borsapy.replay import ReplaySession, create_replay 150from borsapy.scanner import ScanResult, TechnicalScanner, scan 151from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices 152from borsapy.search import ( 153 search, 154 search_bist, 155 search_crypto, 156 search_forex, 157 search_index, 158 search_viop, 159 viop_contracts, 160) 161 162# TradingView streaming for real-time updates 163from borsapy.stream import TradingViewStream, create_stream 164from borsapy.tcmb import TCMB, policy_rate 165from borsapy.technical import ( 166 TechnicalAnalyzer, 167 add_indicators, 168 calculate_adx, 169 calculate_atr, 170 calculate_bollinger_bands, 171 calculate_ema, 172 calculate_macd, 173 calculate_obv, 174 calculate_rsi, 175 calculate_sma, 176 calculate_stochastic, 177 calculate_supertrend, 178 calculate_tilson_t3, 179 calculate_vwap, 180) 181from borsapy.ticker import Ticker 182from borsapy.viop import VIOP 183 184__version__ = "0.7.2" 185__author__ = "Said Surucu" 186 187__all__ = [ 188 # Main classes 189 "Ticker", 190 "Tickers", 191 "FX", 192 "Crypto", 193 "Fund", 194 "Portfolio", 195 "Index", 196 "Inflation", 197 "VIOP", 198 "Bond", 199 "Eurobond", 200 "TCMB", 201 "EconomicCalendar", 202 "Screener", 203 "TradingViewStream", 204 "ReplaySession", 205 # Market functions 206 "companies", 207 "search_companies", 208 "search", 209 "search_bist", 210 "search_crypto", 211 "search_forex", 212 "search_index", 213 "search_viop", 214 "viop_contracts", 215 "banks", 216 "metal_institutions", 217 "crypto_pairs", 218 "search_funds", 219 "screen_funds", 220 "compare_funds", 221 "download", 222 "index", 223 "indices", 224 "all_indices", 225 # Bond functions 226 "bonds", 227 "risk_free_rate", 228 # Eurobond functions 229 "eurobonds", 230 # TCMB functions 231 "policy_rate", 232 # Calendar functions 233 "economic_calendar", 234 # Screener functions 235 "screen_stocks", 236 "screener_criteria", 237 "sectors", 238 "stock_indices", 239 # Technical Scanner 240 "TechnicalScanner", 241 "ScanResult", 242 "scan", 243 # Technical analysis 244 "TechnicalAnalyzer", 245 "add_indicators", 246 "calculate_sma", 247 "calculate_ema", 248 "calculate_rsi", 249 "calculate_macd", 250 "calculate_bollinger_bands", 251 "calculate_atr", 252 "calculate_stochastic", 253 "calculate_obv", 254 "calculate_vwap", 255 "calculate_adx", 256 "calculate_supertrend", 257 "calculate_tilson_t3", 258 # Charts 259 "calculate_heikin_ashi", 260 # Replay 261 "ReplaySession", 262 "create_replay", 263 # Exceptions 264 "BorsapyError", 265 "TickerNotFoundError", 266 "DataNotAvailableError", 267 "APIError", 268 "AuthenticationError", 269 "RateLimitError", 270 "InvalidPeriodError", 271 "InvalidIntervalError", 272 # TradingView authentication (premium) 273 "set_tradingview_auth", 274 "get_tradingview_auth", 275 "clear_tradingview_auth", 276 # TradingView streaming (real-time) 277 "TradingViewStream", 278 "create_stream", 279 # Backtest engine 280 "Backtest", 281 "BacktestResult", 282 "Trade", 283 "backtest", 284]
468class Ticker(TechnicalMixin): 469 """ 470 A yfinance-like interface for Turkish stock data. 471 472 Examples: 473 >>> import borsapy as bp 474 >>> stock = bp.Ticker("THYAO") 475 >>> stock.info 476 {'symbol': 'THYAO', 'last': 268.5, ...} 477 >>> stock.history(period="1mo") 478 Open High Low Close Volume 479 Date 480 2024-12-01 265.00 268.00 264.00 267.50 12345678 481 ... 482 """ 483 484 def __init__(self, symbol: str): 485 """ 486 Initialize a Ticker object. 487 488 Args: 489 symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). 490 The ".IS" or ".E" suffix is optional and will be removed. 491 """ 492 self._symbol = symbol.upper().replace(".IS", "").replace(".E", "") 493 self._tradingview = get_tradingview_provider() 494 self._isyatirim = None # Lazy load for financial statements 495 self._kap = None # Lazy load for KAP disclosures 496 self._isin_provider = None # Lazy load for ISIN lookup 497 self._hedeffiyat = None # Lazy load for analyst price targets 498 self._etf_provider = None # Lazy load for ETF holders 499 500 def _get_isyatirim(self): 501 """Lazy load İş Yatırım provider for financial statements.""" 502 if self._isyatirim is None: 503 from borsapy._providers.isyatirim import get_isyatirim_provider 504 505 self._isyatirim = get_isyatirim_provider() 506 return self._isyatirim 507 508 def _get_kap(self): 509 """Lazy load KAP provider for disclosures and calendar.""" 510 if self._kap is None: 511 from borsapy._providers.kap import get_kap_provider 512 513 self._kap = get_kap_provider() 514 return self._kap 515 516 def _get_isin_provider(self): 517 """Lazy load ISIN provider.""" 518 if self._isin_provider is None: 519 from borsapy._providers.isin import get_isin_provider 520 521 self._isin_provider = get_isin_provider() 522 return self._isin_provider 523 524 def _get_hedeffiyat(self): 525 """Lazy load hedeffiyat.com.tr provider for analyst price targets.""" 526 if self._hedeffiyat is None: 527 from borsapy._providers.hedeffiyat import get_hedeffiyat_provider 528 529 self._hedeffiyat = get_hedeffiyat_provider() 530 return self._hedeffiyat 531 532 def _get_etf_provider(self): 533 """Lazy load TradingView ETF provider for ETF holders.""" 534 if self._etf_provider is None: 535 from borsapy._providers.tradingview_etf import get_tradingview_etf_provider 536 537 self._etf_provider = get_tradingview_etf_provider() 538 return self._etf_provider 539 540 @property 541 def symbol(self) -> str: 542 """Return the ticker symbol.""" 543 return self._symbol 544 545 @property 546 def fast_info(self) -> FastInfo: 547 """ 548 Get fast access to common ticker information. 549 550 Returns a FastInfo object with quick access to frequently used data: 551 - currency, exchange, timezone 552 - last_price, open, day_high, day_low, previous_close, volume 553 - market_cap, shares, pe_ratio, pb_ratio 554 - year_high, year_low (52-week) 555 - fifty_day_average, two_hundred_day_average 556 - free_float, foreign_ratio 557 558 Examples: 559 >>> stock = Ticker("THYAO") 560 >>> stock.fast_info.market_cap 561 370530000000 562 >>> stock.fast_info['pe_ratio'] 563 2.8 564 >>> stock.fast_info.keys() 565 ['currency', 'exchange', 'timezone', ...] 566 """ 567 if not hasattr(self, "_fast_info"): 568 self._fast_info = FastInfo(self) 569 return self._fast_info 570 571 @property 572 def info(self) -> EnrichedInfo: 573 """ 574 Get comprehensive ticker information with yfinance-compatible fields. 575 576 Returns: 577 EnrichedInfo object providing dict-like access to: 578 579 Basic fields (always loaded, fast): 580 - symbol, last, open, high, low, close, volume 581 - change, change_percent, update_time 582 583 yfinance aliases (map to basic fields): 584 - regularMarketPrice, currentPrice -> last 585 - regularMarketOpen -> open 586 - regularMarketDayHigh -> high 587 - regularMarketDayLow -> low 588 - regularMarketPreviousClose -> close 589 - regularMarketVolume -> volume 590 591 Extended fields (lazy-loaded on access): 592 - marketCap, trailingPE, priceToBook, enterpriseToEbitda 593 - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow 594 - fiftyDayAverage, twoHundredDayAverage 595 - floatShares, foreignRatio, netDebt 596 - currency, exchange, timezone 597 598 Dividend fields (lazy-loaded on access): 599 - dividendYield, exDividendDate 600 - trailingAnnualDividendRate, trailingAnnualDividendYield 601 602 Examples: 603 >>> stock = Ticker("THYAO") 604 >>> stock.info['last'] # Basic field - fast 605 268.5 606 >>> stock.info['marketCap'] # Extended field - fetches İş Yatırım 607 370530000000 608 >>> stock.info['trailingPE'] # yfinance compatible name 609 2.8 610 >>> stock.info.get('dividendYield') # Safe access 611 1.28 612 >>> stock.info.todict() # Get all as regular dict 613 {...} 614 """ 615 if not hasattr(self, "_enriched_info"): 616 self._enriched_info = EnrichedInfo(self) 617 return self._enriched_info 618 619 def history( 620 self, 621 period: str = "1mo", 622 interval: str = "1d", 623 start: datetime | str | None = None, 624 end: datetime | str | None = None, 625 actions: bool = False, 626 ) -> pd.DataFrame: 627 """ 628 Get historical OHLCV data. 629 630 Args: 631 period: How much data to fetch. Valid periods: 632 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. 633 Ignored if start is provided. 634 interval: Data granularity. Valid intervals: 635 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 636 start: Start date (string or datetime). 637 end: End date (string or datetime). Defaults to today. 638 actions: If True, include Dividends and Stock Splits columns. 639 Defaults to False. 640 641 Returns: 642 DataFrame with columns: Open, High, Low, Close, Volume. 643 If actions=True, also includes Dividends and Stock Splits columns. 644 Index is the Date. 645 646 Examples: 647 >>> stock = Ticker("THYAO") 648 >>> stock.history(period="1mo") # Last month 649 >>> stock.history(period="1y", interval="1wk") # Weekly for 1 year 650 >>> stock.history(start="2024-01-01", end="2024-06-30") # Date range 651 >>> stock.history(period="1y", actions=True) # With dividends/splits 652 """ 653 # Parse dates if strings 654 start_dt = self._parse_date(start) if start else None 655 end_dt = self._parse_date(end) if end else None 656 657 df = self._tradingview.get_history( 658 symbol=self._symbol, 659 period=period, 660 interval=interval, 661 start=start_dt, 662 end=end_dt, 663 ) 664 665 if actions and not df.empty: 666 df = self._add_actions_to_history(df) 667 668 return df 669 670 def _add_actions_to_history(self, df: pd.DataFrame) -> pd.DataFrame: 671 """ 672 Add Dividends and Stock Splits columns to historical data. 673 674 Args: 675 df: Historical OHLCV DataFrame. 676 677 Returns: 678 DataFrame with added Dividends and Stock Splits columns. 679 """ 680 # Initialize columns with zeros 681 df = df.copy() 682 df["Dividends"] = 0.0 683 df["Stock Splits"] = 0.0 684 685 # Get dividends 686 try: 687 divs = self.dividends 688 if not divs.empty: 689 for div_date, row in divs.iterrows(): 690 # Use date() for timezone-agnostic comparison 691 div_date_only = pd.Timestamp(div_date).date() 692 for idx in df.index: 693 idx_date_only = pd.Timestamp(idx).date() 694 if div_date_only == idx_date_only: 695 df.loc[idx, "Dividends"] = row.get("Amount", 0) 696 break 697 except Exception: 698 pass 699 700 # Get stock splits (capital increases) 701 try: 702 splits = self.splits 703 if not splits.empty: 704 for split_date, row in splits.iterrows(): 705 # Use date() for timezone-agnostic comparison 706 split_date_only = pd.Timestamp(split_date).date() 707 # Calculate split ratio 708 # BonusFromCapital + BonusFromDividend = total bonus percentage 709 bonus_pct = row.get("BonusFromCapital", 0) + row.get( 710 "BonusFromDividend", 0 711 ) 712 if bonus_pct > 0: 713 # Convert percentage to split ratio (e.g., 20% bonus = 1.2 split) 714 split_ratio = 1 + (bonus_pct / 100) 715 for idx in df.index: 716 idx_date_only = pd.Timestamp(idx).date() 717 if split_date_only == idx_date_only: 718 df.loc[idx, "Stock Splits"] = split_ratio 719 break 720 except Exception: 721 pass 722 723 return df 724 725 @cached_property 726 def dividends(self) -> pd.DataFrame: 727 """ 728 Get dividend history. 729 730 Returns: 731 DataFrame with dividend history: 732 - Amount: Dividend per share (TL) 733 - GrossRate: Gross dividend rate (%) 734 - NetRate: Net dividend rate (%) 735 - TotalDividend: Total dividend distributed (TL) 736 737 Examples: 738 >>> stock = Ticker("THYAO") 739 >>> stock.dividends 740 Amount GrossRate NetRate TotalDividend 741 Date 742 2025-09-02 3.442 344.20 292.57 4750000000.0 743 2025-06-16 3.442 344.20 292.57 4750000000.0 744 """ 745 return self._get_isyatirim().get_dividends(self._symbol) 746 747 @cached_property 748 def splits(self) -> pd.DataFrame: 749 """ 750 Get capital increase (split) history. 751 752 Note: Turkish market uses capital increases instead of traditional splits. 753 - RightsIssue: Paid capital increase (bedelli) 754 - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak) 755 - BonusFromDividend: Free shares from dividend (bedelsiz temettüden) 756 757 Returns: 758 DataFrame with capital increase history: 759 - Capital: New capital after increase (TL) 760 - RightsIssue: Rights issue rate (%) 761 - BonusFromCapital: Bonus from capital (%) 762 - BonusFromDividend: Bonus from dividend (%) 763 764 Examples: 765 >>> stock = Ticker("THYAO") 766 >>> stock.splits 767 Capital RightsIssue BonusFromCapital BonusFromDividend 768 Date 769 2013-06-26 1380000000.0 0.0 15.00 0.0 770 2011-07-11 1200000000.0 0.0 0.00 20.0 771 """ 772 return self._get_isyatirim().get_capital_increases(self._symbol) 773 774 @cached_property 775 def actions(self) -> pd.DataFrame: 776 """ 777 Get combined dividends and splits history. 778 779 Returns: 780 DataFrame with combined dividend and split actions: 781 - Dividends: Dividend per share (TL) or 0 782 - Splits: Combined split ratio (0 if no split) 783 784 Examples: 785 >>> stock = Ticker("THYAO") 786 >>> stock.actions 787 Dividends Splits 788 Date 789 2025-09-02 3.442 0.0 790 2013-06-26 0.000 15.0 791 """ 792 dividends = self.dividends 793 splits = self.splits 794 795 # Merge on index (Date) 796 if dividends.empty and splits.empty: 797 return pd.DataFrame(columns=["Dividends", "Splits"]) 798 799 # Extract relevant columns 800 div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float) 801 split_series = ( 802 splits["BonusFromCapital"] + splits["BonusFromDividend"] 803 if not splits.empty 804 else pd.Series(dtype=float) 805 ) 806 807 # Combine into single DataFrame 808 result = pd.DataFrame({"Dividends": div_series, "Splits": split_series}) 809 result = result.fillna(0) 810 result = result.sort_index(ascending=False) 811 812 return result 813 814 def get_balance_sheet( 815 self, quarterly: bool = False, financial_group: str | None = None 816 ) -> pd.DataFrame: 817 """ 818 Get balance sheet data. 819 820 Args: 821 quarterly: If True, return quarterly data. If False, return annual. 822 financial_group: Financial group code. Use "UFRS" for banks, 823 "XI_29" for industrial companies. If None, defaults to XI_29. 824 825 Returns: 826 DataFrame with balance sheet items as rows and periods as columns. 827 828 Examples: 829 >>> stock = bp.Ticker("THYAO") 830 >>> stock.get_balance_sheet() # Annual, industrial 831 >>> stock.get_balance_sheet(quarterly=True) # Quarterly 832 833 >>> bank = bp.Ticker("AKBNK") 834 >>> bank.get_balance_sheet(financial_group="UFRS") # Banks need UFRS 835 """ 836 return self._get_isyatirim().get_financial_statements( 837 symbol=self._symbol, 838 statement_type="balance_sheet", 839 quarterly=quarterly, 840 financial_group=financial_group, 841 ) 842 843 def get_income_stmt( 844 self, quarterly: bool = False, financial_group: str | None = None 845 ) -> pd.DataFrame: 846 """ 847 Get income statement data. 848 849 Args: 850 quarterly: If True, return quarterly data. If False, return annual. 851 financial_group: Financial group code. Use "UFRS" for banks, 852 "XI_29" for industrial companies. If None, defaults to XI_29. 853 854 Returns: 855 DataFrame with income statement items as rows and periods as columns. 856 857 Examples: 858 >>> stock = bp.Ticker("THYAO") 859 >>> stock.get_income_stmt() # Annual 860 >>> stock.get_income_stmt(quarterly=True) # Quarterly 861 862 >>> bank = bp.Ticker("AKBNK") 863 >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS") 864 """ 865 return self._get_isyatirim().get_financial_statements( 866 symbol=self._symbol, 867 statement_type="income_stmt", 868 quarterly=quarterly, 869 financial_group=financial_group, 870 ) 871 872 def get_cashflow( 873 self, quarterly: bool = False, financial_group: str | None = None 874 ) -> pd.DataFrame: 875 """ 876 Get cash flow statement data. 877 878 Args: 879 quarterly: If True, return quarterly data. If False, return annual. 880 financial_group: Financial group code. Use "UFRS" for banks, 881 "XI_29" for industrial companies. If None, defaults to XI_29. 882 883 Returns: 884 DataFrame with cash flow items as rows and periods as columns. 885 886 Examples: 887 >>> stock = bp.Ticker("THYAO") 888 >>> stock.get_cashflow() # Annual 889 >>> stock.get_cashflow(quarterly=True) # Quarterly 890 891 >>> bank = bp.Ticker("AKBNK") 892 >>> bank.get_cashflow(financial_group="UFRS") 893 """ 894 return self._get_isyatirim().get_financial_statements( 895 symbol=self._symbol, 896 statement_type="cashflow", 897 quarterly=quarterly, 898 financial_group=financial_group, 899 ) 900 901 # Legacy property aliases for backward compatibility 902 @cached_property 903 def balance_sheet(self) -> pd.DataFrame: 904 """Annual balance sheet (use get_balance_sheet() for more options).""" 905 return self.get_balance_sheet(quarterly=False) 906 907 @cached_property 908 def quarterly_balance_sheet(self) -> pd.DataFrame: 909 """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).""" 910 return self.get_balance_sheet(quarterly=True) 911 912 @cached_property 913 def income_stmt(self) -> pd.DataFrame: 914 """Annual income statement (use get_income_stmt() for more options).""" 915 return self.get_income_stmt(quarterly=False) 916 917 @cached_property 918 def quarterly_income_stmt(self) -> pd.DataFrame: 919 """Quarterly income statement (use get_income_stmt(quarterly=True) for more options).""" 920 return self.get_income_stmt(quarterly=True) 921 922 @cached_property 923 def cashflow(self) -> pd.DataFrame: 924 """Annual cash flow (use get_cashflow() for more options).""" 925 return self.get_cashflow(quarterly=False) 926 927 @cached_property 928 def quarterly_cashflow(self) -> pd.DataFrame: 929 """Quarterly cash flow (use get_cashflow(quarterly=True) for more options).""" 930 return self.get_cashflow(quarterly=True) 931 932 def _calculate_ttm(self, quarterly_df: pd.DataFrame) -> pd.DataFrame: 933 """ 934 Calculate trailing twelve months (TTM) by summing last 4 quarters. 935 936 Args: 937 quarterly_df: DataFrame with quarterly data (columns in YYYYQN format). 938 939 Returns: 940 DataFrame with single TTM column containing summed values. 941 """ 942 if quarterly_df.empty or len(quarterly_df.columns) < 4: 943 return pd.DataFrame(columns=["TTM"]) 944 945 # First 4 columns = last 4 quarters (most recent first) 946 last_4_quarters = quarterly_df.iloc[:, :4] 947 948 # Convert to numeric, coercing errors to NaN 949 numeric_df = last_4_quarters.apply(pd.to_numeric, errors="coerce") 950 951 return numeric_df.sum(axis=1).to_frame(name="TTM") 952 953 def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame: 954 """ 955 Get trailing twelve months (TTM) income statement. 956 957 Calculates TTM by summing the last 4 quarters of income statement data. 958 959 Args: 960 financial_group: Financial group code. Use "UFRS" for banks, 961 "XI_29" for industrial companies. If None, defaults to XI_29. 962 963 Returns: 964 DataFrame with TTM column containing summed values for each line item. 965 966 Examples: 967 >>> stock = bp.Ticker("THYAO") 968 >>> stock.get_ttm_income_stmt() 969 970 >>> bank = bp.Ticker("AKBNK") 971 >>> bank.get_ttm_income_stmt(financial_group="UFRS") 972 """ 973 quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group) 974 return self._calculate_ttm(quarterly) 975 976 def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame: 977 """ 978 Get trailing twelve months (TTM) cash flow statement. 979 980 Calculates TTM by summing the last 4 quarters of cash flow data. 981 982 Args: 983 financial_group: Financial group code. Use "UFRS" for banks, 984 "XI_29" for industrial companies. If None, defaults to XI_29. 985 986 Returns: 987 DataFrame with TTM column containing summed values for each line item. 988 989 Examples: 990 >>> stock = bp.Ticker("THYAO") 991 >>> stock.get_ttm_cashflow() 992 993 >>> bank = bp.Ticker("AKBNK") 994 >>> bank.get_ttm_cashflow(financial_group="UFRS") 995 """ 996 quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group) 997 return self._calculate_ttm(quarterly) 998 999 # Legacy property aliases 1000 @cached_property 1001 def ttm_income_stmt(self) -> pd.DataFrame: 1002 """TTM income statement (use get_ttm_income_stmt() for banks).""" 1003 return self.get_ttm_income_stmt() 1004 1005 @cached_property 1006 def ttm_cashflow(self) -> pd.DataFrame: 1007 """TTM cash flow (use get_ttm_cashflow() for banks).""" 1008 return self.get_ttm_cashflow() 1009 1010 @cached_property 1011 def major_holders(self) -> pd.DataFrame: 1012 """ 1013 Get major shareholders (ortaklık yapısı). 1014 1015 Returns: 1016 DataFrame with shareholder names and percentages: 1017 - Index: Holder name 1018 - Percentage: Ownership percentage (%) 1019 1020 Examples: 1021 >>> stock = Ticker("THYAO") 1022 >>> stock.major_holders 1023 Percentage 1024 Holder 1025 Diğer 50.88 1026 Türkiye Varlık Fonu 49.12 1027 """ 1028 return self._get_isyatirim().get_major_holders(self._symbol) 1029 1030 @cached_property 1031 def recommendations(self) -> dict: 1032 """ 1033 Get analyst recommendations and target price. 1034 1035 Returns: 1036 Dictionary with: 1037 - recommendation: Buy/Hold/Sell (AL/TUT/SAT) 1038 - target_price: Analyst target price (TL) 1039 - upside_potential: Expected upside (%) 1040 1041 Examples: 1042 >>> stock = Ticker("THYAO") 1043 >>> stock.recommendations 1044 {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01} 1045 """ 1046 return self._get_isyatirim().get_recommendations(self._symbol) 1047 1048 @cached_property 1049 def recommendations_summary(self) -> dict[str, int]: 1050 """ 1051 Get analyst recommendation summary with buy/hold/sell counts. 1052 1053 Aggregates individual analyst recommendations from hedeffiyat.com.tr 1054 into yfinance-compatible categories. 1055 1056 Returns: 1057 Dictionary with counts: 1058 - strongBuy: Strong buy recommendations 1059 - buy: Buy recommendations (includes "Endeks Üstü Getiri") 1060 - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") 1061 - sell: Sell recommendations (includes "Endeks Altı Getiri") 1062 - strongSell: Strong sell recommendations 1063 1064 Examples: 1065 >>> stock = Ticker("THYAO") 1066 >>> stock.recommendations_summary 1067 {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0} 1068 """ 1069 return self._get_hedeffiyat().get_recommendations_summary(self._symbol) 1070 1071 @cached_property 1072 def news(self) -> pd.DataFrame: 1073 """ 1074 Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock. 1075 1076 Fetches directly from KAP - the official disclosure platform for 1077 publicly traded companies in Turkey. 1078 1079 Returns: 1080 DataFrame with columns: 1081 - Date: Disclosure date and time 1082 - Title: Disclosure headline 1083 - URL: Link to full disclosure on KAP 1084 1085 Examples: 1086 >>> stock = Ticker("THYAO") 1087 >>> stock.news 1088 Date Title URL 1089 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1090 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656 1091 """ 1092 return self._get_kap().get_disclosures(self._symbol) 1093 1094 def get_news_content(self, disclosure_id: int | str) -> str | None: 1095 """ 1096 Get full HTML content of a KAP disclosure by ID. 1097 1098 Args: 1099 disclosure_id: KAP disclosure ID from news DataFrame URL. 1100 1101 Returns: 1102 Raw HTML content or None if failed. 1103 1104 Examples: 1105 >>> stock = Ticker("THYAO") 1106 >>> html = stock.get_news_content(1530826) 1107 """ 1108 return self._get_kap().get_disclosure_content(disclosure_id) 1109 1110 @cached_property 1111 def calendar(self) -> pd.DataFrame: 1112 """ 1113 Get expected disclosure calendar for the stock from KAP. 1114 1115 Returns upcoming expected disclosures like financial reports, 1116 annual reports, sustainability reports, and corporate governance reports. 1117 1118 Returns: 1119 DataFrame with columns: 1120 - StartDate: Expected disclosure window start 1121 - EndDate: Expected disclosure window end 1122 - Subject: Type of disclosure (e.g., "Finansal Rapor") 1123 - Period: Report period (e.g., "Yıllık", "3 Aylık") 1124 - Year: Fiscal year 1125 1126 Examples: 1127 >>> stock = Ticker("THYAO") 1128 >>> stock.calendar 1129 StartDate EndDate Subject Period Year 1130 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1131 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 1132 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026 1133 """ 1134 return self._get_kap().get_calendar(self._symbol) 1135 1136 @cached_property 1137 def isin(self) -> str | None: 1138 """ 1139 Get ISIN (International Securities Identification Number) code. 1140 1141 ISIN is a 12-character alphanumeric code that uniquely identifies 1142 a security, standardized by ISO 6166. 1143 1144 Returns: 1145 ISIN code string (e.g., "TRATHYAO91M5") or None if not found. 1146 1147 Examples: 1148 >>> stock = Ticker("THYAO") 1149 >>> stock.isin 1150 'TRATHYAO91M5' 1151 """ 1152 return self._get_isin_provider().get_isin(self._symbol) 1153 1154 @cached_property 1155 def analyst_price_targets(self) -> dict[str, float | int | None]: 1156 """ 1157 Get analyst price target data from hedeffiyat.com.tr. 1158 1159 Returns aggregated price target information from multiple analysts. 1160 1161 Returns: 1162 Dictionary with: 1163 - current: Current stock price 1164 - low: Lowest analyst target price 1165 - high: Highest analyst target price 1166 - mean: Average target price 1167 - median: Median target price 1168 - numberOfAnalysts: Number of analysts covering the stock 1169 1170 Examples: 1171 >>> stock = Ticker("THYAO") 1172 >>> stock.analyst_price_targets 1173 {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 1174 'median': 465.0, 'numberOfAnalysts': 19} 1175 """ 1176 return self._get_hedeffiyat().get_price_targets(self._symbol) 1177 1178 @property 1179 def etf_holders(self) -> pd.DataFrame: 1180 """ 1181 Get international ETFs that hold this stock. 1182 1183 Returns data from TradingView showing which ETFs hold this stock, 1184 including position value, weight, and ETF characteristics. 1185 1186 Returns: 1187 DataFrame with ETF holder information: 1188 - symbol: ETF ticker symbol 1189 - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) 1190 - name: ETF full name 1191 - market_cap_usd: Position value in USD 1192 - holding_weight_pct: Weight percentage (0.09 = 0.09%) 1193 - issuer: ETF issuer (BlackRock, Vanguard, etc.) 1194 - management: Management style (Passive/Active) 1195 - focus: Investment focus (Total Market, Emerging Markets, etc.) 1196 - expense_ratio: Expense ratio (0.09 = 0.09%) 1197 - aum_usd: Total assets under management (USD) 1198 - price: Current ETF price 1199 - change_pct: Change percentage 1200 1201 Examples: 1202 >>> stock = Ticker("ASELS") 1203 >>> holders = stock.etf_holders 1204 >>> holders[['symbol', 'name', 'holding_weight_pct']].head() 1205 symbol name holding_weight_pct 1206 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1207 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000 1208 1209 >>> print(f"Total ETFs: {len(holders)}") 1210 Total ETFs: 118 1211 """ 1212 return self._get_etf_provider().get_etf_holders(self._symbol) 1213 1214 @cached_property 1215 def earnings_dates(self) -> pd.DataFrame: 1216 """ 1217 Get upcoming earnings announcement dates. 1218 1219 Derived from KAP calendar, showing expected financial report dates. 1220 Compatible with yfinance earnings_dates format. 1221 1222 Returns: 1223 DataFrame with index as Earnings Date and columns: 1224 - EPS Estimate: Always None (not available for BIST) 1225 - Reported EPS: Always None (not available for BIST) 1226 - Surprise (%): Always None (not available for BIST) 1227 1228 Examples: 1229 >>> stock = Ticker("THYAO") 1230 >>> stock.earnings_dates 1231 EPS Estimate Reported EPS Surprise(%) 1232 Earnings Date 1233 2026-03-11 None None None 1234 2026-05-11 None None None 1235 """ 1236 cal = self.calendar 1237 if cal.empty: 1238 return pd.DataFrame( 1239 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1240 ) 1241 1242 # Filter for financial reports only 1243 financial_reports = cal[ 1244 cal["Subject"].str.contains("Finansal Rapor", case=False, na=False) 1245 ] 1246 1247 if financial_reports.empty: 1248 return pd.DataFrame( 1249 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1250 ) 1251 1252 # Use EndDate as the earnings date (latest expected date) 1253 earnings_dates = [] 1254 for _, row in financial_reports.iterrows(): 1255 end_date = row.get("EndDate", "") 1256 if end_date: 1257 try: 1258 # Parse Turkish date format (DD.MM.YYYY) 1259 parsed = datetime.strptime(end_date, "%d.%m.%Y") 1260 earnings_dates.append(parsed) 1261 except ValueError: 1262 continue 1263 1264 if not earnings_dates: 1265 return pd.DataFrame( 1266 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1267 ) 1268 1269 result = pd.DataFrame( 1270 { 1271 "EPS Estimate": [None] * len(earnings_dates), 1272 "Reported EPS": [None] * len(earnings_dates), 1273 "Surprise(%)": [None] * len(earnings_dates), 1274 }, 1275 index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"), 1276 ) 1277 result = result.sort_index() 1278 return result 1279 1280 def _parse_date(self, date: str | datetime) -> datetime: 1281 """Parse a date string to datetime.""" 1282 if isinstance(date, datetime): 1283 return date 1284 # Try common formats 1285 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 1286 try: 1287 return datetime.strptime(date, fmt) 1288 except ValueError: 1289 continue 1290 raise ValueError(f"Could not parse date: {date}") 1291 1292 def _get_ta_symbol_info(self) -> tuple[str, str]: 1293 """Get TradingView symbol and screener for TA signals. 1294 1295 Returns: 1296 Tuple of (tv_symbol, screener) for TradingView Scanner API. 1297 """ 1298 return (f"BIST:{self._symbol}", "turkey") 1299 1300 def __repr__(self) -> str: 1301 return f"Ticker('{self._symbol}')"
A yfinance-like interface for Turkish stock data.
Examples:
import borsapy as bp stock = bp.Ticker("THYAO") stock.info {'symbol': 'THYAO', 'last': 268.5, ...} stock.history(period="1mo") Open High Low Close Volume Date 2024-12-01 265.00 268.00 264.00 267.50 12345678 ...
484 def __init__(self, symbol: str): 485 """ 486 Initialize a Ticker object. 487 488 Args: 489 symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). 490 The ".IS" or ".E" suffix is optional and will be removed. 491 """ 492 self._symbol = symbol.upper().replace(".IS", "").replace(".E", "") 493 self._tradingview = get_tradingview_provider() 494 self._isyatirim = None # Lazy load for financial statements 495 self._kap = None # Lazy load for KAP disclosures 496 self._isin_provider = None # Lazy load for ISIN lookup 497 self._hedeffiyat = None # Lazy load for analyst price targets 498 self._etf_provider = None # Lazy load for ETF holders
Initialize a Ticker object.
Args: symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). The ".IS" or ".E" suffix is optional and will be removed.
540 @property 541 def symbol(self) -> str: 542 """Return the ticker symbol.""" 543 return self._symbol
Return the ticker symbol.
545 @property 546 def fast_info(self) -> FastInfo: 547 """ 548 Get fast access to common ticker information. 549 550 Returns a FastInfo object with quick access to frequently used data: 551 - currency, exchange, timezone 552 - last_price, open, day_high, day_low, previous_close, volume 553 - market_cap, shares, pe_ratio, pb_ratio 554 - year_high, year_low (52-week) 555 - fifty_day_average, two_hundred_day_average 556 - free_float, foreign_ratio 557 558 Examples: 559 >>> stock = Ticker("THYAO") 560 >>> stock.fast_info.market_cap 561 370530000000 562 >>> stock.fast_info['pe_ratio'] 563 2.8 564 >>> stock.fast_info.keys() 565 ['currency', 'exchange', 'timezone', ...] 566 """ 567 if not hasattr(self, "_fast_info"): 568 self._fast_info = FastInfo(self) 569 return self._fast_info
Get fast access to common ticker information.
Returns a FastInfo object with quick access to frequently used data:
- currency, exchange, timezone
- last_price, open, day_high, day_low, previous_close, volume
- market_cap, shares, pe_ratio, pb_ratio
- year_high, year_low (52-week)
- fifty_day_average, two_hundred_day_average
- free_float, foreign_ratio
Examples:
stock = Ticker("THYAO") stock.fast_info.market_cap 370530000000 stock.fast_info['pe_ratio'] 2.8 stock.fast_info.keys() ['currency', 'exchange', 'timezone', ...]
571 @property 572 def info(self) -> EnrichedInfo: 573 """ 574 Get comprehensive ticker information with yfinance-compatible fields. 575 576 Returns: 577 EnrichedInfo object providing dict-like access to: 578 579 Basic fields (always loaded, fast): 580 - symbol, last, open, high, low, close, volume 581 - change, change_percent, update_time 582 583 yfinance aliases (map to basic fields): 584 - regularMarketPrice, currentPrice -> last 585 - regularMarketOpen -> open 586 - regularMarketDayHigh -> high 587 - regularMarketDayLow -> low 588 - regularMarketPreviousClose -> close 589 - regularMarketVolume -> volume 590 591 Extended fields (lazy-loaded on access): 592 - marketCap, trailingPE, priceToBook, enterpriseToEbitda 593 - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow 594 - fiftyDayAverage, twoHundredDayAverage 595 - floatShares, foreignRatio, netDebt 596 - currency, exchange, timezone 597 598 Dividend fields (lazy-loaded on access): 599 - dividendYield, exDividendDate 600 - trailingAnnualDividendRate, trailingAnnualDividendYield 601 602 Examples: 603 >>> stock = Ticker("THYAO") 604 >>> stock.info['last'] # Basic field - fast 605 268.5 606 >>> stock.info['marketCap'] # Extended field - fetches İş Yatırım 607 370530000000 608 >>> stock.info['trailingPE'] # yfinance compatible name 609 2.8 610 >>> stock.info.get('dividendYield') # Safe access 611 1.28 612 >>> stock.info.todict() # Get all as regular dict 613 {...} 614 """ 615 if not hasattr(self, "_enriched_info"): 616 self._enriched_info = EnrichedInfo(self) 617 return self._enriched_info
Get comprehensive ticker information with yfinance-compatible fields.
Returns: EnrichedInfo object providing dict-like access to:
Basic fields (always loaded, fast):
- symbol, last, open, high, low, close, volume
- change, change_percent, update_time
yfinance aliases (map to basic fields):
- regularMarketPrice, currentPrice -> last
- regularMarketOpen -> open
- regularMarketDayHigh -> high
- regularMarketDayLow -> low
- regularMarketPreviousClose -> close
- regularMarketVolume -> volume
Extended fields (lazy-loaded on access):
- marketCap, trailingPE, priceToBook, enterpriseToEbitda
- sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
- fiftyDayAverage, twoHundredDayAverage
- floatShares, foreignRatio, netDebt
- currency, exchange, timezone
Dividend fields (lazy-loaded on access):
- dividendYield, exDividendDate
- trailingAnnualDividendRate, trailingAnnualDividendYield
Examples:
stock = Ticker("THYAO") stock.info['last'] # Basic field - fast 268.5 stock.info['marketCap'] # Extended field - fetches İş Yatırım 370530000000 stock.info['trailingPE'] # yfinance compatible name 2.8 stock.info.get('dividendYield') # Safe access 1.28 stock.info.todict() # Get all as regular dict {...}
619 def history( 620 self, 621 period: str = "1mo", 622 interval: str = "1d", 623 start: datetime | str | None = None, 624 end: datetime | str | None = None, 625 actions: bool = False, 626 ) -> pd.DataFrame: 627 """ 628 Get historical OHLCV data. 629 630 Args: 631 period: How much data to fetch. Valid periods: 632 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. 633 Ignored if start is provided. 634 interval: Data granularity. Valid intervals: 635 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 636 start: Start date (string or datetime). 637 end: End date (string or datetime). Defaults to today. 638 actions: If True, include Dividends and Stock Splits columns. 639 Defaults to False. 640 641 Returns: 642 DataFrame with columns: Open, High, Low, Close, Volume. 643 If actions=True, also includes Dividends and Stock Splits columns. 644 Index is the Date. 645 646 Examples: 647 >>> stock = Ticker("THYAO") 648 >>> stock.history(period="1mo") # Last month 649 >>> stock.history(period="1y", interval="1wk") # Weekly for 1 year 650 >>> stock.history(start="2024-01-01", end="2024-06-30") # Date range 651 >>> stock.history(period="1y", actions=True) # With dividends/splits 652 """ 653 # Parse dates if strings 654 start_dt = self._parse_date(start) if start else None 655 end_dt = self._parse_date(end) if end else None 656 657 df = self._tradingview.get_history( 658 symbol=self._symbol, 659 period=period, 660 interval=interval, 661 start=start_dt, 662 end=end_dt, 663 ) 664 665 if actions and not df.empty: 666 df = self._add_actions_to_history(df) 667 668 return df
Get historical OHLCV data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. Ignored if start is provided. interval: Data granularity. Valid intervals: 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today. actions: If True, include Dividends and Stock Splits columns. Defaults to False.
Returns: DataFrame with columns: Open, High, Low, Close, Volume. If actions=True, also includes Dividends and Stock Splits columns. Index is the Date.
Examples:
stock = Ticker("THYAO") stock.history(period="1mo") # Last month stock.history(period="1y", interval="1wk") # Weekly for 1 year stock.history(start="2024-01-01", end="2024-06-30") # Date range stock.history(period="1y", actions=True) # With dividends/splits
725 @cached_property 726 def dividends(self) -> pd.DataFrame: 727 """ 728 Get dividend history. 729 730 Returns: 731 DataFrame with dividend history: 732 - Amount: Dividend per share (TL) 733 - GrossRate: Gross dividend rate (%) 734 - NetRate: Net dividend rate (%) 735 - TotalDividend: Total dividend distributed (TL) 736 737 Examples: 738 >>> stock = Ticker("THYAO") 739 >>> stock.dividends 740 Amount GrossRate NetRate TotalDividend 741 Date 742 2025-09-02 3.442 344.20 292.57 4750000000.0 743 2025-06-16 3.442 344.20 292.57 4750000000.0 744 """ 745 return self._get_isyatirim().get_dividends(self._symbol)
Get dividend history.
Returns: DataFrame with dividend history: - Amount: Dividend per share (TL) - GrossRate: Gross dividend rate (%) - NetRate: Net dividend rate (%) - TotalDividend: Total dividend distributed (TL)
Examples:
stock = Ticker("THYAO") stock.dividends Amount GrossRate NetRate TotalDividend Date 2025-09-02 3.442 344.20 292.57 4750000000.0 2025-06-16 3.442 344.20 292.57 4750000000.0
747 @cached_property 748 def splits(self) -> pd.DataFrame: 749 """ 750 Get capital increase (split) history. 751 752 Note: Turkish market uses capital increases instead of traditional splits. 753 - RightsIssue: Paid capital increase (bedelli) 754 - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak) 755 - BonusFromDividend: Free shares from dividend (bedelsiz temettüden) 756 757 Returns: 758 DataFrame with capital increase history: 759 - Capital: New capital after increase (TL) 760 - RightsIssue: Rights issue rate (%) 761 - BonusFromCapital: Bonus from capital (%) 762 - BonusFromDividend: Bonus from dividend (%) 763 764 Examples: 765 >>> stock = Ticker("THYAO") 766 >>> stock.splits 767 Capital RightsIssue BonusFromCapital BonusFromDividend 768 Date 769 2013-06-26 1380000000.0 0.0 15.00 0.0 770 2011-07-11 1200000000.0 0.0 0.00 20.0 771 """ 772 return self._get_isyatirim().get_capital_increases(self._symbol)
Get capital increase (split) history.
Note: Turkish market uses capital increases instead of traditional splits.
- RightsIssue: Paid capital increase (bedelli)
- BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
- BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
Returns: DataFrame with capital increase history: - Capital: New capital after increase (TL) - RightsIssue: Rights issue rate (%) - BonusFromCapital: Bonus from capital (%) - BonusFromDividend: Bonus from dividend (%)
Examples:
stock = Ticker("THYAO") stock.splits Capital RightsIssue BonusFromCapital BonusFromDividend Date 2013-06-26 1380000000.0 0.0 15.00 0.0 2011-07-11 1200000000.0 0.0 0.00 20.0
774 @cached_property 775 def actions(self) -> pd.DataFrame: 776 """ 777 Get combined dividends and splits history. 778 779 Returns: 780 DataFrame with combined dividend and split actions: 781 - Dividends: Dividend per share (TL) or 0 782 - Splits: Combined split ratio (0 if no split) 783 784 Examples: 785 >>> stock = Ticker("THYAO") 786 >>> stock.actions 787 Dividends Splits 788 Date 789 2025-09-02 3.442 0.0 790 2013-06-26 0.000 15.0 791 """ 792 dividends = self.dividends 793 splits = self.splits 794 795 # Merge on index (Date) 796 if dividends.empty and splits.empty: 797 return pd.DataFrame(columns=["Dividends", "Splits"]) 798 799 # Extract relevant columns 800 div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float) 801 split_series = ( 802 splits["BonusFromCapital"] + splits["BonusFromDividend"] 803 if not splits.empty 804 else pd.Series(dtype=float) 805 ) 806 807 # Combine into single DataFrame 808 result = pd.DataFrame({"Dividends": div_series, "Splits": split_series}) 809 result = result.fillna(0) 810 result = result.sort_index(ascending=False) 811 812 return result
Get combined dividends and splits history.
Returns: DataFrame with combined dividend and split actions: - Dividends: Dividend per share (TL) or 0 - Splits: Combined split ratio (0 if no split)
Examples:
stock = Ticker("THYAO") stock.actions Dividends Splits Date 2025-09-02 3.442 0.0 2013-06-26 0.000 15.0
814 def get_balance_sheet( 815 self, quarterly: bool = False, financial_group: str | None = None 816 ) -> pd.DataFrame: 817 """ 818 Get balance sheet data. 819 820 Args: 821 quarterly: If True, return quarterly data. If False, return annual. 822 financial_group: Financial group code. Use "UFRS" for banks, 823 "XI_29" for industrial companies. If None, defaults to XI_29. 824 825 Returns: 826 DataFrame with balance sheet items as rows and periods as columns. 827 828 Examples: 829 >>> stock = bp.Ticker("THYAO") 830 >>> stock.get_balance_sheet() # Annual, industrial 831 >>> stock.get_balance_sheet(quarterly=True) # Quarterly 832 833 >>> bank = bp.Ticker("AKBNK") 834 >>> bank.get_balance_sheet(financial_group="UFRS") # Banks need UFRS 835 """ 836 return self._get_isyatirim().get_financial_statements( 837 symbol=self._symbol, 838 statement_type="balance_sheet", 839 quarterly=quarterly, 840 financial_group=financial_group, 841 )
Get balance sheet data.
Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.
Returns: DataFrame with balance sheet items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_balance_sheet() # Annual, industrial stock.get_balance_sheet(quarterly=True) # Quarterly
>>> bank = bp.Ticker("AKBNK") >>> bank.get_balance_sheet(financial_group="UFRS") # Banks need UFRS
843 def get_income_stmt( 844 self, quarterly: bool = False, financial_group: str | None = None 845 ) -> pd.DataFrame: 846 """ 847 Get income statement data. 848 849 Args: 850 quarterly: If True, return quarterly data. If False, return annual. 851 financial_group: Financial group code. Use "UFRS" for banks, 852 "XI_29" for industrial companies. If None, defaults to XI_29. 853 854 Returns: 855 DataFrame with income statement items as rows and periods as columns. 856 857 Examples: 858 >>> stock = bp.Ticker("THYAO") 859 >>> stock.get_income_stmt() # Annual 860 >>> stock.get_income_stmt(quarterly=True) # Quarterly 861 862 >>> bank = bp.Ticker("AKBNK") 863 >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS") 864 """ 865 return self._get_isyatirim().get_financial_statements( 866 symbol=self._symbol, 867 statement_type="income_stmt", 868 quarterly=quarterly, 869 financial_group=financial_group, 870 )
Get income statement data.
Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.
Returns: DataFrame with income statement items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_income_stmt() # Annual stock.get_income_stmt(quarterly=True) # Quarterly
>>> bank = bp.Ticker("AKBNK") >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
872 def get_cashflow( 873 self, quarterly: bool = False, financial_group: str | None = None 874 ) -> pd.DataFrame: 875 """ 876 Get cash flow statement data. 877 878 Args: 879 quarterly: If True, return quarterly data. If False, return annual. 880 financial_group: Financial group code. Use "UFRS" for banks, 881 "XI_29" for industrial companies. If None, defaults to XI_29. 882 883 Returns: 884 DataFrame with cash flow items as rows and periods as columns. 885 886 Examples: 887 >>> stock = bp.Ticker("THYAO") 888 >>> stock.get_cashflow() # Annual 889 >>> stock.get_cashflow(quarterly=True) # Quarterly 890 891 >>> bank = bp.Ticker("AKBNK") 892 >>> bank.get_cashflow(financial_group="UFRS") 893 """ 894 return self._get_isyatirim().get_financial_statements( 895 symbol=self._symbol, 896 statement_type="cashflow", 897 quarterly=quarterly, 898 financial_group=financial_group, 899 )
Get cash flow statement data.
Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.
Returns: DataFrame with cash flow items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_cashflow() # Annual stock.get_cashflow(quarterly=True) # Quarterly
>>> bank = bp.Ticker("AKBNK") >>> bank.get_cashflow(financial_group="UFRS")
902 @cached_property 903 def balance_sheet(self) -> pd.DataFrame: 904 """Annual balance sheet (use get_balance_sheet() for more options).""" 905 return self.get_balance_sheet(quarterly=False)
Annual balance sheet (use get_balance_sheet() for more options).
907 @cached_property 908 def quarterly_balance_sheet(self) -> pd.DataFrame: 909 """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).""" 910 return self.get_balance_sheet(quarterly=True)
Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).
912 @cached_property 913 def income_stmt(self) -> pd.DataFrame: 914 """Annual income statement (use get_income_stmt() for more options).""" 915 return self.get_income_stmt(quarterly=False)
Annual income statement (use get_income_stmt() for more options).
917 @cached_property 918 def quarterly_income_stmt(self) -> pd.DataFrame: 919 """Quarterly income statement (use get_income_stmt(quarterly=True) for more options).""" 920 return self.get_income_stmt(quarterly=True)
Quarterly income statement (use get_income_stmt(quarterly=True) for more options).
922 @cached_property 923 def cashflow(self) -> pd.DataFrame: 924 """Annual cash flow (use get_cashflow() for more options).""" 925 return self.get_cashflow(quarterly=False)
Annual cash flow (use get_cashflow() for more options).
927 @cached_property 928 def quarterly_cashflow(self) -> pd.DataFrame: 929 """Quarterly cash flow (use get_cashflow(quarterly=True) for more options).""" 930 return self.get_cashflow(quarterly=True)
Quarterly cash flow (use get_cashflow(quarterly=True) for more options).
953 def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame: 954 """ 955 Get trailing twelve months (TTM) income statement. 956 957 Calculates TTM by summing the last 4 quarters of income statement data. 958 959 Args: 960 financial_group: Financial group code. Use "UFRS" for banks, 961 "XI_29" for industrial companies. If None, defaults to XI_29. 962 963 Returns: 964 DataFrame with TTM column containing summed values for each line item. 965 966 Examples: 967 >>> stock = bp.Ticker("THYAO") 968 >>> stock.get_ttm_income_stmt() 969 970 >>> bank = bp.Ticker("AKBNK") 971 >>> bank.get_ttm_income_stmt(financial_group="UFRS") 972 """ 973 quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group) 974 return self._calculate_ttm(quarterly)
Get trailing twelve months (TTM) income statement.
Calculates TTM by summing the last 4 quarters of income statement data.
Args: financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.
Returns: DataFrame with TTM column containing summed values for each line item.
Examples:
stock = bp.Ticker("THYAO") stock.get_ttm_income_stmt()
>>> bank = bp.Ticker("AKBNK") >>> bank.get_ttm_income_stmt(financial_group="UFRS")
976 def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame: 977 """ 978 Get trailing twelve months (TTM) cash flow statement. 979 980 Calculates TTM by summing the last 4 quarters of cash flow data. 981 982 Args: 983 financial_group: Financial group code. Use "UFRS" for banks, 984 "XI_29" for industrial companies. If None, defaults to XI_29. 985 986 Returns: 987 DataFrame with TTM column containing summed values for each line item. 988 989 Examples: 990 >>> stock = bp.Ticker("THYAO") 991 >>> stock.get_ttm_cashflow() 992 993 >>> bank = bp.Ticker("AKBNK") 994 >>> bank.get_ttm_cashflow(financial_group="UFRS") 995 """ 996 quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group) 997 return self._calculate_ttm(quarterly)
Get trailing twelve months (TTM) cash flow statement.
Calculates TTM by summing the last 4 quarters of cash flow data.
Args: financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.
Returns: DataFrame with TTM column containing summed values for each line item.
Examples:
stock = bp.Ticker("THYAO") stock.get_ttm_cashflow()
>>> bank = bp.Ticker("AKBNK") >>> bank.get_ttm_cashflow(financial_group="UFRS")
1000 @cached_property 1001 def ttm_income_stmt(self) -> pd.DataFrame: 1002 """TTM income statement (use get_ttm_income_stmt() for banks).""" 1003 return self.get_ttm_income_stmt()
TTM income statement (use get_ttm_income_stmt() for banks).
1005 @cached_property 1006 def ttm_cashflow(self) -> pd.DataFrame: 1007 """TTM cash flow (use get_ttm_cashflow() for banks).""" 1008 return self.get_ttm_cashflow()
TTM cash flow (use get_ttm_cashflow() for banks).
1010 @cached_property 1011 def major_holders(self) -> pd.DataFrame: 1012 """ 1013 Get major shareholders (ortaklık yapısı). 1014 1015 Returns: 1016 DataFrame with shareholder names and percentages: 1017 - Index: Holder name 1018 - Percentage: Ownership percentage (%) 1019 1020 Examples: 1021 >>> stock = Ticker("THYAO") 1022 >>> stock.major_holders 1023 Percentage 1024 Holder 1025 Diğer 50.88 1026 Türkiye Varlık Fonu 49.12 1027 """ 1028 return self._get_isyatirim().get_major_holders(self._symbol)
Get major shareholders (ortaklık yapısı).
Returns: DataFrame with shareholder names and percentages: - Index: Holder name - Percentage: Ownership percentage (%)
Examples:
stock = Ticker("THYAO") stock.major_holders Percentage Holder Diğer 50.88 Türkiye Varlık Fonu 49.12
1030 @cached_property 1031 def recommendations(self) -> dict: 1032 """ 1033 Get analyst recommendations and target price. 1034 1035 Returns: 1036 Dictionary with: 1037 - recommendation: Buy/Hold/Sell (AL/TUT/SAT) 1038 - target_price: Analyst target price (TL) 1039 - upside_potential: Expected upside (%) 1040 1041 Examples: 1042 >>> stock = Ticker("THYAO") 1043 >>> stock.recommendations 1044 {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01} 1045 """ 1046 return self._get_isyatirim().get_recommendations(self._symbol)
Get analyst recommendations and target price.
Returns: Dictionary with: - recommendation: Buy/Hold/Sell (AL/TUT/SAT) - target_price: Analyst target price (TL) - upside_potential: Expected upside (%)
Examples:
stock = Ticker("THYAO") stock.recommendations {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
1048 @cached_property 1049 def recommendations_summary(self) -> dict[str, int]: 1050 """ 1051 Get analyst recommendation summary with buy/hold/sell counts. 1052 1053 Aggregates individual analyst recommendations from hedeffiyat.com.tr 1054 into yfinance-compatible categories. 1055 1056 Returns: 1057 Dictionary with counts: 1058 - strongBuy: Strong buy recommendations 1059 - buy: Buy recommendations (includes "Endeks Üstü Getiri") 1060 - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") 1061 - sell: Sell recommendations (includes "Endeks Altı Getiri") 1062 - strongSell: Strong sell recommendations 1063 1064 Examples: 1065 >>> stock = Ticker("THYAO") 1066 >>> stock.recommendations_summary 1067 {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0} 1068 """ 1069 return self._get_hedeffiyat().get_recommendations_summary(self._symbol)
Get analyst recommendation summary with buy/hold/sell counts.
Aggregates individual analyst recommendations from hedeffiyat.com.tr into yfinance-compatible categories.
Returns: Dictionary with counts: - strongBuy: Strong buy recommendations - buy: Buy recommendations (includes "Endeks Üstü Getiri") - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") - sell: Sell recommendations (includes "Endeks Altı Getiri") - strongSell: Strong sell recommendations
Examples:
stock = Ticker("THYAO") stock.recommendations_summary {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
1071 @cached_property 1072 def news(self) -> pd.DataFrame: 1073 """ 1074 Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock. 1075 1076 Fetches directly from KAP - the official disclosure platform for 1077 publicly traded companies in Turkey. 1078 1079 Returns: 1080 DataFrame with columns: 1081 - Date: Disclosure date and time 1082 - Title: Disclosure headline 1083 - URL: Link to full disclosure on KAP 1084 1085 Examples: 1086 >>> stock = Ticker("THYAO") 1087 >>> stock.news 1088 Date Title URL 1089 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1090 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656 1091 """ 1092 return self._get_kap().get_disclosures(self._symbol)
Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
Fetches directly from KAP - the official disclosure platform for publicly traded companies in Turkey.
Returns: DataFrame with columns: - Date: Disclosure date and time - Title: Disclosure headline - URL: Link to full disclosure on KAP
Examples:
stock = Ticker("THYAO") stock.news Date Title URL 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656
1094 def get_news_content(self, disclosure_id: int | str) -> str | None: 1095 """ 1096 Get full HTML content of a KAP disclosure by ID. 1097 1098 Args: 1099 disclosure_id: KAP disclosure ID from news DataFrame URL. 1100 1101 Returns: 1102 Raw HTML content or None if failed. 1103 1104 Examples: 1105 >>> stock = Ticker("THYAO") 1106 >>> html = stock.get_news_content(1530826) 1107 """ 1108 return self._get_kap().get_disclosure_content(disclosure_id)
Get full HTML content of a KAP disclosure by ID.
Args: disclosure_id: KAP disclosure ID from news DataFrame URL.
Returns: Raw HTML content or None if failed.
Examples:
stock = Ticker("THYAO") html = stock.get_news_content(1530826)
1110 @cached_property 1111 def calendar(self) -> pd.DataFrame: 1112 """ 1113 Get expected disclosure calendar for the stock from KAP. 1114 1115 Returns upcoming expected disclosures like financial reports, 1116 annual reports, sustainability reports, and corporate governance reports. 1117 1118 Returns: 1119 DataFrame with columns: 1120 - StartDate: Expected disclosure window start 1121 - EndDate: Expected disclosure window end 1122 - Subject: Type of disclosure (e.g., "Finansal Rapor") 1123 - Period: Report period (e.g., "Yıllık", "3 Aylık") 1124 - Year: Fiscal year 1125 1126 Examples: 1127 >>> stock = Ticker("THYAO") 1128 >>> stock.calendar 1129 StartDate EndDate Subject Period Year 1130 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1131 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 1132 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026 1133 """ 1134 return self._get_kap().get_calendar(self._symbol)
Get expected disclosure calendar for the stock from KAP.
Returns upcoming expected disclosures like financial reports, annual reports, sustainability reports, and corporate governance reports.
Returns: DataFrame with columns: - StartDate: Expected disclosure window start - EndDate: Expected disclosure window end - Subject: Type of disclosure (e.g., "Finansal Rapor") - Period: Report period (e.g., "Yıllık", "3 Aylık") - Year: Fiscal year
Examples:
stock = Ticker("THYAO") stock.calendar StartDate EndDate Subject Period Year 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026
1136 @cached_property 1137 def isin(self) -> str | None: 1138 """ 1139 Get ISIN (International Securities Identification Number) code. 1140 1141 ISIN is a 12-character alphanumeric code that uniquely identifies 1142 a security, standardized by ISO 6166. 1143 1144 Returns: 1145 ISIN code string (e.g., "TRATHYAO91M5") or None if not found. 1146 1147 Examples: 1148 >>> stock = Ticker("THYAO") 1149 >>> stock.isin 1150 'TRATHYAO91M5' 1151 """ 1152 return self._get_isin_provider().get_isin(self._symbol)
Get ISIN (International Securities Identification Number) code.
ISIN is a 12-character alphanumeric code that uniquely identifies a security, standardized by ISO 6166.
Returns: ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
Examples:
stock = Ticker("THYAO") stock.isin 'TRATHYAO91M5'
1154 @cached_property 1155 def analyst_price_targets(self) -> dict[str, float | int | None]: 1156 """ 1157 Get analyst price target data from hedeffiyat.com.tr. 1158 1159 Returns aggregated price target information from multiple analysts. 1160 1161 Returns: 1162 Dictionary with: 1163 - current: Current stock price 1164 - low: Lowest analyst target price 1165 - high: Highest analyst target price 1166 - mean: Average target price 1167 - median: Median target price 1168 - numberOfAnalysts: Number of analysts covering the stock 1169 1170 Examples: 1171 >>> stock = Ticker("THYAO") 1172 >>> stock.analyst_price_targets 1173 {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 1174 'median': 465.0, 'numberOfAnalysts': 19} 1175 """ 1176 return self._get_hedeffiyat().get_price_targets(self._symbol)
Get analyst price target data from hedeffiyat.com.tr.
Returns aggregated price target information from multiple analysts.
Returns: Dictionary with: - current: Current stock price - low: Lowest analyst target price - high: Highest analyst target price - mean: Average target price - median: Median target price - numberOfAnalysts: Number of analysts covering the stock
Examples:
stock = Ticker("THYAO") stock.analyst_price_targets {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 'median': 465.0, 'numberOfAnalysts': 19}
1178 @property 1179 def etf_holders(self) -> pd.DataFrame: 1180 """ 1181 Get international ETFs that hold this stock. 1182 1183 Returns data from TradingView showing which ETFs hold this stock, 1184 including position value, weight, and ETF characteristics. 1185 1186 Returns: 1187 DataFrame with ETF holder information: 1188 - symbol: ETF ticker symbol 1189 - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) 1190 - name: ETF full name 1191 - market_cap_usd: Position value in USD 1192 - holding_weight_pct: Weight percentage (0.09 = 0.09%) 1193 - issuer: ETF issuer (BlackRock, Vanguard, etc.) 1194 - management: Management style (Passive/Active) 1195 - focus: Investment focus (Total Market, Emerging Markets, etc.) 1196 - expense_ratio: Expense ratio (0.09 = 0.09%) 1197 - aum_usd: Total assets under management (USD) 1198 - price: Current ETF price 1199 - change_pct: Change percentage 1200 1201 Examples: 1202 >>> stock = Ticker("ASELS") 1203 >>> holders = stock.etf_holders 1204 >>> holders[['symbol', 'name', 'holding_weight_pct']].head() 1205 symbol name holding_weight_pct 1206 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1207 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000 1208 1209 >>> print(f"Total ETFs: {len(holders)}") 1210 Total ETFs: 118 1211 """ 1212 return self._get_etf_provider().get_etf_holders(self._symbol)
Get international ETFs that hold this stock.
Returns data from TradingView showing which ETFs hold this stock, including position value, weight, and ETF characteristics.
Returns: DataFrame with ETF holder information: - symbol: ETF ticker symbol - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) - name: ETF full name - market_cap_usd: Position value in USD - holding_weight_pct: Weight percentage (0.09 = 0.09%) - issuer: ETF issuer (BlackRock, Vanguard, etc.) - management: Management style (Passive/Active) - focus: Investment focus (Total Market, Emerging Markets, etc.) - expense_ratio: Expense ratio (0.09 = 0.09%) - aum_usd: Total assets under management (USD) - price: Current ETF price - change_pct: Change percentage
Examples:
stock = Ticker("ASELS") holders = stock.etf_holders holders[['symbol', 'name', 'holding_weight_pct']].head() symbol name holding_weight_pct 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000
>>> print(f"Total ETFs: {len(holders)}") Total ETFs: 118
1214 @cached_property 1215 def earnings_dates(self) -> pd.DataFrame: 1216 """ 1217 Get upcoming earnings announcement dates. 1218 1219 Derived from KAP calendar, showing expected financial report dates. 1220 Compatible with yfinance earnings_dates format. 1221 1222 Returns: 1223 DataFrame with index as Earnings Date and columns: 1224 - EPS Estimate: Always None (not available for BIST) 1225 - Reported EPS: Always None (not available for BIST) 1226 - Surprise (%): Always None (not available for BIST) 1227 1228 Examples: 1229 >>> stock = Ticker("THYAO") 1230 >>> stock.earnings_dates 1231 EPS Estimate Reported EPS Surprise(%) 1232 Earnings Date 1233 2026-03-11 None None None 1234 2026-05-11 None None None 1235 """ 1236 cal = self.calendar 1237 if cal.empty: 1238 return pd.DataFrame( 1239 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1240 ) 1241 1242 # Filter for financial reports only 1243 financial_reports = cal[ 1244 cal["Subject"].str.contains("Finansal Rapor", case=False, na=False) 1245 ] 1246 1247 if financial_reports.empty: 1248 return pd.DataFrame( 1249 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1250 ) 1251 1252 # Use EndDate as the earnings date (latest expected date) 1253 earnings_dates = [] 1254 for _, row in financial_reports.iterrows(): 1255 end_date = row.get("EndDate", "") 1256 if end_date: 1257 try: 1258 # Parse Turkish date format (DD.MM.YYYY) 1259 parsed = datetime.strptime(end_date, "%d.%m.%Y") 1260 earnings_dates.append(parsed) 1261 except ValueError: 1262 continue 1263 1264 if not earnings_dates: 1265 return pd.DataFrame( 1266 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1267 ) 1268 1269 result = pd.DataFrame( 1270 { 1271 "EPS Estimate": [None] * len(earnings_dates), 1272 "Reported EPS": [None] * len(earnings_dates), 1273 "Surprise(%)": [None] * len(earnings_dates), 1274 }, 1275 index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"), 1276 ) 1277 result = result.sort_index() 1278 return result
Get upcoming earnings announcement dates.
Derived from KAP calendar, showing expected financial report dates. Compatible with yfinance earnings_dates format.
Returns: DataFrame with index as Earnings Date and columns: - EPS Estimate: Always None (not available for BIST) - Reported EPS: Always None (not available for BIST) - Surprise (%): Always None (not available for BIST)
Examples:
stock = Ticker("THYAO") stock.earnings_dates EPS Estimate Reported EPS Surprise(%) Earnings Date 2026-03-11 None None None 2026-05-11 None None None
12class Tickers: 13 """ 14 Container for multiple Ticker objects. 15 16 Examples: 17 >>> import borsapy as bp 18 >>> tickers = bp.Tickers("THYAO GARAN AKBNK") 19 >>> tickers.tickers["THYAO"].info 20 >>> tickers.symbols 21 ['THYAO', 'GARAN', 'AKBNK'] 22 23 >>> tickers = bp.Tickers(["THYAO", "GARAN", "AKBNK"]) 24 >>> for symbol, ticker in tickers: 25 ... print(symbol, ticker.info['last']) 26 """ 27 28 def __init__(self, symbols: str | list[str]): 29 """ 30 Initialize Tickers with multiple symbols. 31 32 Args: 33 symbols: Space-separated string or list of symbols. 34 Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN", "AKBNK"] 35 """ 36 if isinstance(symbols, str): 37 self._symbols = [s.strip().upper() for s in symbols.split() if s.strip()] 38 else: 39 self._symbols = [s.strip().upper() for s in symbols if s.strip()] 40 41 self._tickers: dict[str, Ticker] = {} 42 for symbol in self._symbols: 43 self._tickers[symbol] = Ticker(symbol) 44 45 @property 46 def symbols(self) -> list[str]: 47 """Return list of symbols.""" 48 return self._symbols.copy() 49 50 @property 51 def tickers(self) -> dict[str, Ticker]: 52 """Return dictionary of Ticker objects keyed by symbol.""" 53 return self._tickers 54 55 def history( 56 self, 57 period: str = "1mo", 58 interval: str = "1d", 59 start: datetime | str | None = None, 60 end: datetime | str | None = None, 61 group_by: str = "column", 62 ) -> pd.DataFrame: 63 """ 64 Get historical data for all tickers. 65 66 Args: 67 period: Data period (1d, 5d, 1mo, 3mo, 6mo, 1y, etc.) 68 interval: Data interval (1d, 1wk, 1mo) 69 start: Start date 70 end: End date 71 group_by: How to group columns ('column' or 'ticker') 72 73 Returns: 74 DataFrame with multi-level columns. 75 """ 76 return download( 77 self._symbols, 78 period=period, 79 interval=interval, 80 start=start, 81 end=end, 82 group_by=group_by, 83 ) 84 85 def __iter__(self): 86 """Iterate over (symbol, ticker) pairs.""" 87 return iter(self._tickers.items()) 88 89 def __len__(self) -> int: 90 """Return number of tickers.""" 91 return len(self._tickers) 92 93 def __getitem__(self, symbol: str) -> Ticker: 94 """Get ticker by symbol.""" 95 symbol = symbol.upper() 96 if symbol not in self._tickers: 97 raise KeyError(f"Symbol not found: {symbol}") 98 return self._tickers[symbol] 99 100 def __repr__(self) -> str: 101 return f"Tickers({self._symbols})"
Container for multiple Ticker objects.
Examples:
import borsapy as bp tickers = bp.Tickers("THYAO GARAN AKBNK") tickers.tickers["THYAO"].info tickers.symbols ['THYAO', 'GARAN', 'AKBNK']
>>> tickers = bp.Tickers(["THYAO", "GARAN", "AKBNK"]) >>> for symbol, ticker in tickers: ... print(symbol, ticker.info['last'])
28 def __init__(self, symbols: str | list[str]): 29 """ 30 Initialize Tickers with multiple symbols. 31 32 Args: 33 symbols: Space-separated string or list of symbols. 34 Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN", "AKBNK"] 35 """ 36 if isinstance(symbols, str): 37 self._symbols = [s.strip().upper() for s in symbols.split() if s.strip()] 38 else: 39 self._symbols = [s.strip().upper() for s in symbols if s.strip()] 40 41 self._tickers: dict[str, Ticker] = {} 42 for symbol in self._symbols: 43 self._tickers[symbol] = Ticker(symbol)
Initialize Tickers with multiple symbols.
Args: symbols: Space-separated string or list of symbols. Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN", "AKBNK"]
45 @property 46 def symbols(self) -> list[str]: 47 """Return list of symbols.""" 48 return self._symbols.copy()
Return list of symbols.
50 @property 51 def tickers(self) -> dict[str, Ticker]: 52 """Return dictionary of Ticker objects keyed by symbol.""" 53 return self._tickers
Return dictionary of Ticker objects keyed by symbol.
55 def history( 56 self, 57 period: str = "1mo", 58 interval: str = "1d", 59 start: datetime | str | None = None, 60 end: datetime | str | None = None, 61 group_by: str = "column", 62 ) -> pd.DataFrame: 63 """ 64 Get historical data for all tickers. 65 66 Args: 67 period: Data period (1d, 5d, 1mo, 3mo, 6mo, 1y, etc.) 68 interval: Data interval (1d, 1wk, 1mo) 69 start: Start date 70 end: End date 71 group_by: How to group columns ('column' or 'ticker') 72 73 Returns: 74 DataFrame with multi-level columns. 75 """ 76 return download( 77 self._symbols, 78 period=period, 79 interval=interval, 80 start=start, 81 end=end, 82 group_by=group_by, 83 )
Get historical data for all tickers.
Args: period: Data period (1d, 5d, 1mo, 3mo, 6mo, 1y, etc.) interval: Data interval (1d, 1wk, 1mo) start: Start date end: End date group_by: How to group columns ('column' or 'ticker')
Returns: DataFrame with multi-level columns.
73class FX(TechnicalMixin): 74 """ 75 A yfinance-like interface for forex and commodity data. 76 77 Supported assets: 78 - Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD (+ 58 more via canlidoviz) 79 - Precious Metals: gram-altin, gumus, ons-altin, gram-platin, XAG-USD, XPT-USD, XPD-USD 80 - Energy: BRENT 81 82 Examples: 83 >>> import borsapy as bp 84 >>> usd = bp.FX("USD") 85 >>> usd.current 86 {'symbol': 'USD', 'last': 34.85, ...} 87 >>> usd.history(period="1mo") 88 Open High Low Close 89 Date 90 2024-12-01 34.50 34.80 34.40 34.75 91 ... 92 93 >>> gold = bp.FX("gram-altin") 94 >>> gold.current 95 {'symbol': 'gram-altin', 'last': 2850.50, ...} 96 """ 97 98 def __init__(self, asset: str): 99 """ 100 Initialize an FX object. 101 102 Args: 103 asset: Asset code (USD, EUR, gram-altin, BRENT, etc.) 104 """ 105 self._asset = asset 106 self._canlidoviz = get_canlidoviz_provider() 107 self._dovizcom = get_dovizcom_provider() 108 self._tradingview = get_tradingview_provider() 109 self._current_cache: dict[str, Any] | None = None 110 111 def _use_canlidoviz(self) -> bool: 112 """Check if canlidoviz should be used for this asset.""" 113 asset_upper = self._asset.upper() 114 # Currencies 115 if asset_upper in self._canlidoviz.CURRENCY_IDS: 116 return True 117 # Metals supported by canlidoviz (TRY prices) 118 if self._asset in self._canlidoviz.METAL_IDS: 119 return True 120 # Energy supported by canlidoviz (USD prices) 121 if asset_upper in self._canlidoviz.ENERGY_IDS: 122 return True 123 # Commodities supported by canlidoviz (USD prices) 124 if asset_upper in self._canlidoviz.COMMODITY_IDS: 125 return True 126 return False 127 128 def _get_tradingview_symbol(self) -> tuple[str, str] | None: 129 """Get TradingView exchange and symbol for this asset. 130 131 Returns: 132 Tuple of (exchange, symbol) or None if not supported. 133 """ 134 asset_upper = self._asset.upper() 135 136 # Check currency map first 137 if asset_upper in TV_CURRENCY_MAP: 138 return TV_CURRENCY_MAP[asset_upper] 139 140 # Check commodity map 141 if self._asset in TV_COMMODITY_MAP: 142 return TV_COMMODITY_MAP[self._asset] 143 if asset_upper in TV_COMMODITY_MAP: 144 return TV_COMMODITY_MAP[asset_upper] 145 146 return None 147 148 @property 149 def asset(self) -> str: 150 """Return the asset code.""" 151 return self._asset 152 153 @property 154 def symbol(self) -> str: 155 """Return the asset code (alias for asset).""" 156 return self._asset 157 158 @property 159 def current(self) -> dict[str, Any]: 160 """ 161 Get current price information. 162 163 Returns: 164 Dictionary with current market data: 165 - symbol: Asset code 166 - last: Last price 167 - open: Opening price 168 - high: Day high 169 - low: Day low 170 - update_time: Last update timestamp 171 """ 172 if self._current_cache is None: 173 if self._use_canlidoviz(): 174 self._current_cache = self._canlidoviz.get_current(self._asset) 175 else: 176 try: 177 self._current_cache = self._dovizcom.get_current(self._asset) 178 except Exception: 179 # Fallback to bank_rates for currencies not supported by APIs 180 self._current_cache = self._current_from_bank_rates() 181 return self._current_cache 182 183 def _current_from_bank_rates(self) -> dict[str, Any]: 184 """Calculate current price from bank rates as fallback.""" 185 from datetime import datetime 186 187 rates = self._dovizcom.get_bank_rates(self._asset) 188 if rates.empty: 189 raise ValueError(f"No data available for {self._asset}") 190 191 # Calculate average mid price from all banks 192 mids = (rates["buy"] + rates["sell"]) / 2 193 avg_mid = float(mids.mean()) 194 195 return { 196 "symbol": self._asset, 197 "last": avg_mid, 198 "open": avg_mid, 199 "high": float(rates["sell"].max()), 200 "low": float(rates["buy"].min()), 201 "update_time": datetime.now(), 202 "source": "bank_rates_avg", 203 } 204 205 @property 206 def info(self) -> dict[str, Any]: 207 """Alias for current property (yfinance compatibility).""" 208 return self.current 209 210 @property 211 def bank_rates(self) -> pd.DataFrame: 212 """ 213 Get exchange rates from all banks. 214 215 Returns: 216 DataFrame with columns: bank, bank_name, currency, buy, sell, spread 217 218 Examples: 219 >>> usd = FX("USD") 220 >>> usd.bank_rates 221 bank bank_name currency buy sell spread 222 0 akbank Akbank USD 41.6610 44.1610 5.99 223 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 224 ... 225 """ 226 return self._dovizcom.get_bank_rates(self._asset) 227 228 def bank_rate(self, bank: str) -> dict[str, Any]: 229 """ 230 Get exchange rate from a specific bank. 231 232 Args: 233 bank: Bank code (akbank, garanti, isbank, ziraat, etc.) 234 235 Returns: 236 Dictionary with keys: bank, currency, buy, sell, spread 237 238 Examples: 239 >>> usd = FX("USD") 240 >>> usd.bank_rate("akbank") 241 {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99} 242 """ 243 return self._dovizcom.get_bank_rates(self._asset, bank=bank) 244 245 @staticmethod 246 def banks() -> list[str]: 247 """ 248 Get list of supported banks. 249 250 Returns: 251 List of bank codes. 252 253 Examples: 254 >>> FX.banks() 255 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 256 """ 257 from borsapy._providers.dovizcom import get_dovizcom_provider 258 259 return get_dovizcom_provider().get_banks() 260 261 @property 262 def institution_rates(self) -> pd.DataFrame: 263 """ 264 Get precious metal rates from all institutions (kuyumcular, bankalar). 265 266 Only available for precious metals: gram-altin, gram-gumus, ons-altin, 267 gram-platin 268 269 Returns: 270 DataFrame with columns: institution, institution_name, asset, buy, sell, spread 271 272 Examples: 273 >>> gold = FX("gram-altin") 274 >>> gold.institution_rates 275 institution institution_name asset buy sell spread 276 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 277 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 278 ... 279 """ 280 return self._dovizcom.get_metal_institution_rates(self._asset) 281 282 def institution_rate(self, institution: str) -> dict[str, Any]: 283 """ 284 Get precious metal rate from a specific institution. 285 286 Args: 287 institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.) 288 289 Returns: 290 Dictionary with keys: institution, institution_name, asset, buy, sell, spread 291 292 Examples: 293 >>> gold = FX("gram-altin") 294 >>> gold.institution_rate("akbank") 295 {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 296 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32} 297 """ 298 return self._dovizcom.get_metal_institution_rates(self._asset, institution=institution) 299 300 @staticmethod 301 def metal_institutions() -> list[str]: 302 """ 303 Get list of supported precious metal assets for institution rates. 304 305 Returns: 306 List of asset codes that support institution_rates. 307 308 Examples: 309 >>> FX.metal_institutions() 310 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 311 """ 312 from borsapy._providers.dovizcom import get_dovizcom_provider 313 314 return get_dovizcom_provider().get_metal_institutions() 315 316 def history( 317 self, 318 period: str = "1mo", 319 interval: str = "1d", 320 start: datetime | str | None = None, 321 end: datetime | str | None = None, 322 ) -> pd.DataFrame: 323 """ 324 Get historical OHLC data. 325 326 Args: 327 period: How much data to fetch. Valid periods: 328 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. 329 Ignored if start is provided. 330 interval: Data interval. Valid intervals: 331 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. 332 Note: Intraday intervals (1m-4h) use TradingView. 333 Daily and above use canlidoviz/dovizcom. 334 start: Start date (string or datetime). 335 end: End date (string or datetime). Defaults to today. 336 337 Returns: 338 DataFrame with columns: Open, High, Low, Close, Volume. 339 Index is the Date. 340 341 Examples: 342 >>> fx = FX("USD") 343 >>> fx.history(period="1mo") # Last month daily 344 >>> fx.history(period="1d", interval="1m") # Today's minute data 345 >>> fx.history(period="5d", interval="1h") # 5 days hourly 346 >>> fx.history(start="2024-01-01", end="2024-06-30") # Date range 347 """ 348 start_dt = self._parse_date(start) if start else None 349 end_dt = self._parse_date(end) if end else None 350 351 # Use TradingView for intraday intervals 352 intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h") 353 if interval in intraday_intervals: 354 tv_info = self._get_tradingview_symbol() 355 if tv_info is None: 356 raise ValueError( 357 f"Intraday data not available for {self._asset}. " 358 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}" 359 ) 360 361 exchange, symbol = tv_info 362 return self._tradingview.get_history( 363 symbol=symbol, 364 period=period, 365 interval=interval, 366 start=start_dt, 367 end=end_dt, 368 exchange=exchange, 369 ) 370 371 # Use canlidoviz/dovizcom for daily and above 372 if self._use_canlidoviz(): 373 return self._canlidoviz.get_history( 374 asset=self._asset, 375 period=period, 376 start=start_dt, 377 end=end_dt, 378 ) 379 else: 380 return self._dovizcom.get_history( 381 asset=self._asset, 382 period=period, 383 start=start_dt, 384 end=end_dt, 385 ) 386 387 def institution_history( 388 self, 389 institution: str, 390 period: str = "1mo", 391 start: datetime | str | None = None, 392 end: datetime | str | None = None, 393 ) -> pd.DataFrame: 394 """ 395 Get historical OHLC data from a specific institution. 396 397 Supports both precious metals and currencies. 398 399 Args: 400 institution: Institution slug (akbank, kapalicarsi, harem, etc.) 401 period: How much data to fetch. Valid periods: 402 1d, 5d, 1mo, 3mo, 6mo, 1y. 403 Ignored if start is provided. 404 start: Start date (string or datetime). 405 end: End date (string or datetime). Defaults to today. 406 407 Returns: 408 DataFrame with columns: Open, High, Low, Close. 409 Index is the Date. 410 Note: Banks typically return only Close values (Open/High/Low = 0). 411 412 Examples: 413 >>> # Metal history 414 >>> gold = FX("gram-altin") 415 >>> gold.institution_history("akbank", period="1mo") 416 >>> gold.institution_history("kapalicarsi", start="2024-01-01") 417 418 >>> # Currency history 419 >>> usd = FX("USD") 420 >>> usd.institution_history("akbank", period="1mo") 421 >>> usd.institution_history("garanti-bbva", period="5d") 422 """ 423 start_dt = self._parse_date(start) if start else None 424 end_dt = self._parse_date(end) if end else None 425 426 # Use canlidoviz for currencies and precious metals (bank-specific rates) 427 asset_upper = self._asset.upper() 428 use_canlidoviz = ( 429 asset_upper in self._canlidoviz.CURRENCY_IDS 430 or self._asset in ("gram-altin", "gumus", "gram-platin") 431 ) 432 433 if use_canlidoviz: 434 # Check if canlidoviz has bank ID for this asset 435 try: 436 return self._canlidoviz.get_history( 437 asset=self._asset, 438 period=period, 439 start=start_dt, 440 end=end_dt, 441 institution=institution, 442 ) 443 except Exception: 444 # Fall back to dovizcom if canlidoviz doesn't support this bank 445 pass 446 447 # Use dovizcom for other metals and unsupported banks 448 return self._dovizcom.get_institution_history( 449 asset=self._asset, 450 institution=institution, 451 period=period, 452 start=start_dt, 453 end=end_dt, 454 ) 455 456 def _parse_date(self, date: str | datetime) -> datetime: 457 """Parse a date string to datetime.""" 458 if isinstance(date, datetime): 459 return date 460 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 461 try: 462 return datetime.strptime(date, fmt) 463 except ValueError: 464 continue 465 raise ValueError(f"Could not parse date: {date}") 466 467 def _get_ta_symbol_info(self) -> tuple[str, str]: 468 """Get TradingView symbol and screener for TA signals. 469 470 Returns: 471 Tuple of (tv_symbol, screener) for TradingView Scanner API. 472 473 Raises: 474 NotImplementedError: If TA signals not available for this asset. 475 """ 476 tv_info = self._get_tradingview_symbol() 477 if tv_info is None: 478 raise NotImplementedError( 479 f"TA signals not available for {self._asset}. " 480 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}. " 481 f"Supported commodities: {list(TV_COMMODITY_MAP.keys())}." 482 ) 483 exchange, symbol = tv_info 484 return (f"{exchange}:{symbol}", "forex") 485 486 def __repr__(self) -> str: 487 return f"FX('{self._asset}')"
A yfinance-like interface for forex and commodity data.
Supported assets:
- Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD (+ 58 more via canlidoviz)
- Precious Metals: gram-altin, gumus, ons-altin, gram-platin, XAG-USD, XPT-USD, XPD-USD
- Energy: BRENT
Examples:
import borsapy as bp usd = bp.FX("USD") usd.current {'symbol': 'USD', 'last': 34.85, ...} usd.history(period="1mo") Open High Low Close Date 2024-12-01 34.50 34.80 34.40 34.75 ...
>>> gold = bp.FX("gram-altin") >>> gold.current {'symbol': 'gram-altin', 'last': 2850.50, ...}
98 def __init__(self, asset: str): 99 """ 100 Initialize an FX object. 101 102 Args: 103 asset: Asset code (USD, EUR, gram-altin, BRENT, etc.) 104 """ 105 self._asset = asset 106 self._canlidoviz = get_canlidoviz_provider() 107 self._dovizcom = get_dovizcom_provider() 108 self._tradingview = get_tradingview_provider() 109 self._current_cache: dict[str, Any] | None = None
Initialize an FX object.
Args: asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)
153 @property 154 def symbol(self) -> str: 155 """Return the asset code (alias for asset).""" 156 return self._asset
Return the asset code (alias for asset).
158 @property 159 def current(self) -> dict[str, Any]: 160 """ 161 Get current price information. 162 163 Returns: 164 Dictionary with current market data: 165 - symbol: Asset code 166 - last: Last price 167 - open: Opening price 168 - high: Day high 169 - low: Day low 170 - update_time: Last update timestamp 171 """ 172 if self._current_cache is None: 173 if self._use_canlidoviz(): 174 self._current_cache = self._canlidoviz.get_current(self._asset) 175 else: 176 try: 177 self._current_cache = self._dovizcom.get_current(self._asset) 178 except Exception: 179 # Fallback to bank_rates for currencies not supported by APIs 180 self._current_cache = self._current_from_bank_rates() 181 return self._current_cache
Get current price information.
Returns: Dictionary with current market data: - symbol: Asset code - last: Last price - open: Opening price - high: Day high - low: Day low - update_time: Last update timestamp
205 @property 206 def info(self) -> dict[str, Any]: 207 """Alias for current property (yfinance compatibility).""" 208 return self.current
Alias for current property (yfinance compatibility).
210 @property 211 def bank_rates(self) -> pd.DataFrame: 212 """ 213 Get exchange rates from all banks. 214 215 Returns: 216 DataFrame with columns: bank, bank_name, currency, buy, sell, spread 217 218 Examples: 219 >>> usd = FX("USD") 220 >>> usd.bank_rates 221 bank bank_name currency buy sell spread 222 0 akbank Akbank USD 41.6610 44.1610 5.99 223 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 224 ... 225 """ 226 return self._dovizcom.get_bank_rates(self._asset)
Get exchange rates from all banks.
Returns: DataFrame with columns: bank, bank_name, currency, buy, sell, spread
Examples:
usd = FX("USD") usd.bank_rates bank bank_name currency buy sell spread 0 akbank Akbank USD 41.6610 44.1610 5.99 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 ...
228 def bank_rate(self, bank: str) -> dict[str, Any]: 229 """ 230 Get exchange rate from a specific bank. 231 232 Args: 233 bank: Bank code (akbank, garanti, isbank, ziraat, etc.) 234 235 Returns: 236 Dictionary with keys: bank, currency, buy, sell, spread 237 238 Examples: 239 >>> usd = FX("USD") 240 >>> usd.bank_rate("akbank") 241 {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99} 242 """ 243 return self._dovizcom.get_bank_rates(self._asset, bank=bank)
Get exchange rate from a specific bank.
Args: bank: Bank code (akbank, garanti, isbank, ziraat, etc.)
Returns: Dictionary with keys: bank, currency, buy, sell, spread
Examples:
usd = FX("USD") usd.bank_rate("akbank") {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99}
245 @staticmethod 246 def banks() -> list[str]: 247 """ 248 Get list of supported banks. 249 250 Returns: 251 List of bank codes. 252 253 Examples: 254 >>> FX.banks() 255 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 256 """ 257 from borsapy._providers.dovizcom import get_dovizcom_provider 258 259 return get_dovizcom_provider().get_banks()
Get list of supported banks.
Returns: List of bank codes.
Examples:
FX.banks() ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
261 @property 262 def institution_rates(self) -> pd.DataFrame: 263 """ 264 Get precious metal rates from all institutions (kuyumcular, bankalar). 265 266 Only available for precious metals: gram-altin, gram-gumus, ons-altin, 267 gram-platin 268 269 Returns: 270 DataFrame with columns: institution, institution_name, asset, buy, sell, spread 271 272 Examples: 273 >>> gold = FX("gram-altin") 274 >>> gold.institution_rates 275 institution institution_name asset buy sell spread 276 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 277 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 278 ... 279 """ 280 return self._dovizcom.get_metal_institution_rates(self._asset)
Get precious metal rates from all institutions (kuyumcular, bankalar).
Only available for precious metals: gram-altin, gram-gumus, ons-altin, gram-platin
Returns: DataFrame with columns: institution, institution_name, asset, buy, sell, spread
Examples:
gold = FX("gram-altin") gold.institution_rates institution institution_name asset buy sell spread 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 ...
282 def institution_rate(self, institution: str) -> dict[str, Any]: 283 """ 284 Get precious metal rate from a specific institution. 285 286 Args: 287 institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.) 288 289 Returns: 290 Dictionary with keys: institution, institution_name, asset, buy, sell, spread 291 292 Examples: 293 >>> gold = FX("gram-altin") 294 >>> gold.institution_rate("akbank") 295 {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 296 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32} 297 """ 298 return self._dovizcom.get_metal_institution_rates(self._asset, institution=institution)
Get precious metal rate from a specific institution.
Args: institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.)
Returns: Dictionary with keys: institution, institution_name, asset, buy, sell, spread
Examples:
gold = FX("gram-altin") gold.institution_rate("akbank") {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32}
300 @staticmethod 301 def metal_institutions() -> list[str]: 302 """ 303 Get list of supported precious metal assets for institution rates. 304 305 Returns: 306 List of asset codes that support institution_rates. 307 308 Examples: 309 >>> FX.metal_institutions() 310 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 311 """ 312 from borsapy._providers.dovizcom import get_dovizcom_provider 313 314 return get_dovizcom_provider().get_metal_institutions()
Get list of supported precious metal assets for institution rates.
Returns: List of asset codes that support institution_rates.
Examples:
FX.metal_institutions() ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']
316 def history( 317 self, 318 period: str = "1mo", 319 interval: str = "1d", 320 start: datetime | str | None = None, 321 end: datetime | str | None = None, 322 ) -> pd.DataFrame: 323 """ 324 Get historical OHLC data. 325 326 Args: 327 period: How much data to fetch. Valid periods: 328 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. 329 Ignored if start is provided. 330 interval: Data interval. Valid intervals: 331 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. 332 Note: Intraday intervals (1m-4h) use TradingView. 333 Daily and above use canlidoviz/dovizcom. 334 start: Start date (string or datetime). 335 end: End date (string or datetime). Defaults to today. 336 337 Returns: 338 DataFrame with columns: Open, High, Low, Close, Volume. 339 Index is the Date. 340 341 Examples: 342 >>> fx = FX("USD") 343 >>> fx.history(period="1mo") # Last month daily 344 >>> fx.history(period="1d", interval="1m") # Today's minute data 345 >>> fx.history(period="5d", interval="1h") # 5 days hourly 346 >>> fx.history(start="2024-01-01", end="2024-06-30") # Date range 347 """ 348 start_dt = self._parse_date(start) if start else None 349 end_dt = self._parse_date(end) if end else None 350 351 # Use TradingView for intraday intervals 352 intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h") 353 if interval in intraday_intervals: 354 tv_info = self._get_tradingview_symbol() 355 if tv_info is None: 356 raise ValueError( 357 f"Intraday data not available for {self._asset}. " 358 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}" 359 ) 360 361 exchange, symbol = tv_info 362 return self._tradingview.get_history( 363 symbol=symbol, 364 period=period, 365 interval=interval, 366 start=start_dt, 367 end=end_dt, 368 exchange=exchange, 369 ) 370 371 # Use canlidoviz/dovizcom for daily and above 372 if self._use_canlidoviz(): 373 return self._canlidoviz.get_history( 374 asset=self._asset, 375 period=period, 376 start=start_dt, 377 end=end_dt, 378 ) 379 else: 380 return self._dovizcom.get_history( 381 asset=self._asset, 382 period=period, 383 start=start_dt, 384 end=end_dt, 385 )
Get historical OHLC data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. Ignored if start is provided. interval: Data interval. Valid intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. Note: Intraday intervals (1m-4h) use TradingView. Daily and above use canlidoviz/dovizcom. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.
Returns: DataFrame with columns: Open, High, Low, Close, Volume. Index is the Date.
Examples:
fx = FX("USD") fx.history(period="1mo") # Last month daily fx.history(period="1d", interval="1m") # Today's minute data fx.history(period="5d", interval="1h") # 5 days hourly fx.history(start="2024-01-01", end="2024-06-30") # Date range
387 def institution_history( 388 self, 389 institution: str, 390 period: str = "1mo", 391 start: datetime | str | None = None, 392 end: datetime | str | None = None, 393 ) -> pd.DataFrame: 394 """ 395 Get historical OHLC data from a specific institution. 396 397 Supports both precious metals and currencies. 398 399 Args: 400 institution: Institution slug (akbank, kapalicarsi, harem, etc.) 401 period: How much data to fetch. Valid periods: 402 1d, 5d, 1mo, 3mo, 6mo, 1y. 403 Ignored if start is provided. 404 start: Start date (string or datetime). 405 end: End date (string or datetime). Defaults to today. 406 407 Returns: 408 DataFrame with columns: Open, High, Low, Close. 409 Index is the Date. 410 Note: Banks typically return only Close values (Open/High/Low = 0). 411 412 Examples: 413 >>> # Metal history 414 >>> gold = FX("gram-altin") 415 >>> gold.institution_history("akbank", period="1mo") 416 >>> gold.institution_history("kapalicarsi", start="2024-01-01") 417 418 >>> # Currency history 419 >>> usd = FX("USD") 420 >>> usd.institution_history("akbank", period="1mo") 421 >>> usd.institution_history("garanti-bbva", period="5d") 422 """ 423 start_dt = self._parse_date(start) if start else None 424 end_dt = self._parse_date(end) if end else None 425 426 # Use canlidoviz for currencies and precious metals (bank-specific rates) 427 asset_upper = self._asset.upper() 428 use_canlidoviz = ( 429 asset_upper in self._canlidoviz.CURRENCY_IDS 430 or self._asset in ("gram-altin", "gumus", "gram-platin") 431 ) 432 433 if use_canlidoviz: 434 # Check if canlidoviz has bank ID for this asset 435 try: 436 return self._canlidoviz.get_history( 437 asset=self._asset, 438 period=period, 439 start=start_dt, 440 end=end_dt, 441 institution=institution, 442 ) 443 except Exception: 444 # Fall back to dovizcom if canlidoviz doesn't support this bank 445 pass 446 447 # Use dovizcom for other metals and unsupported banks 448 return self._dovizcom.get_institution_history( 449 asset=self._asset, 450 institution=institution, 451 period=period, 452 start=start_dt, 453 end=end_dt, 454 )
Get historical OHLC data from a specific institution.
Supports both precious metals and currencies.
Args: institution: Institution slug (akbank, kapalicarsi, harem, etc.) period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.
Returns: DataFrame with columns: Open, High, Low, Close. Index is the Date. Note: Banks typically return only Close values (Open/High/Low = 0).
Examples:
Metal history
gold = FX("gram-altin") gold.institution_history("akbank", period="1mo") gold.institution_history("kapalicarsi", start="2024-01-01")
>>> # Currency history >>> usd = FX("USD") >>> usd.institution_history("akbank", period="1mo") >>> usd.institution_history("garanti-bbva", period="5d")
13class Crypto(TechnicalMixin): 14 """ 15 A yfinance-like interface for cryptocurrency data from BtcTurk. 16 17 Examples: 18 >>> import borsapy as bp 19 >>> btc = bp.Crypto("BTCTRY") 20 >>> btc.current 21 {'symbol': 'BTCTRY', 'last': 3500000.0, ...} 22 >>> btc.history(period="1mo") 23 Open High Low Close Volume 24 Date 25 2024-12-01 3400000.0 3550000.0 3380000.0 3500000.0 1234.5678 26 ... 27 28 >>> eth = bp.Crypto("ETHTRY") 29 >>> eth.current['last'] 30 125000.0 31 """ 32 33 def __init__(self, pair: str): 34 """ 35 Initialize a Crypto object. 36 37 Args: 38 pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). 39 Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY 40 """ 41 self._pair = pair.upper() 42 self._provider = get_btcturk_provider() 43 self._current_cache: dict[str, Any] | None = None 44 45 @property 46 def pair(self) -> str: 47 """Return the trading pair.""" 48 return self._pair 49 50 @property 51 def symbol(self) -> str: 52 """Return the trading pair (alias).""" 53 return self._pair 54 55 @property 56 def current(self) -> dict[str, Any]: 57 """ 58 Get current ticker information. 59 60 Returns: 61 Dictionary with current market data: 62 - symbol: Trading pair 63 - last: Last traded price 64 - open: Opening price 65 - high: 24h high 66 - low: 24h low 67 - bid: Best bid price 68 - ask: Best ask price 69 - volume: 24h volume 70 - change: Price change 71 - change_percent: Percent change 72 """ 73 if self._current_cache is None: 74 self._current_cache = self._provider.get_ticker(self._pair) 75 return self._current_cache 76 77 @property 78 def info(self) -> dict[str, Any]: 79 """Alias for current property (yfinance compatibility).""" 80 return self.current 81 82 def history( 83 self, 84 period: str = "1mo", 85 interval: str = "1d", 86 start: datetime | str | None = None, 87 end: datetime | str | None = None, 88 ) -> pd.DataFrame: 89 """ 90 Get historical OHLCV data. 91 92 Args: 93 period: How much data to fetch. Valid periods: 94 1d, 5d, 1mo, 3mo, 6mo, 1y. 95 Ignored if start is provided. 96 interval: Data granularity. Valid intervals: 97 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. 98 start: Start date (string or datetime). 99 end: End date (string or datetime). Defaults to now. 100 101 Returns: 102 DataFrame with columns: Open, High, Low, Close, Volume. 103 Index is the Date. 104 105 Examples: 106 >>> crypto = Crypto("BTCTRY") 107 >>> crypto.history(period="1mo") # Last month 108 >>> crypto.history(period="1y", interval="1wk") # Weekly for 1 year 109 >>> crypto.history(start="2024-01-01", end="2024-06-30") # Date range 110 """ 111 start_dt = self._parse_date(start) if start else None 112 end_dt = self._parse_date(end) if end else None 113 114 return self._provider.get_history( 115 pair=self._pair, 116 period=period, 117 interval=interval, 118 start=start_dt, 119 end=end_dt, 120 ) 121 122 def _parse_date(self, date: str | datetime) -> datetime: 123 """Parse a date string to datetime.""" 124 if isinstance(date, datetime): 125 return date 126 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 127 try: 128 return datetime.strptime(date, fmt) 129 except ValueError: 130 continue 131 raise ValueError(f"Could not parse date: {date}") 132 133 def _get_ta_symbol_info(self) -> tuple[str, str]: 134 """Get TradingView symbol and screener for TA signals. 135 136 Maps BtcTurk pairs to Binance USDT pairs for better TradingView coverage. 137 138 Returns: 139 Tuple of (tv_symbol, screener) for TradingView Scanner API. 140 """ 141 # Extract base currency from pair (e.g., "BTCTRY" -> "BTC") 142 base = self._pair.replace("TRY", "").replace("USDT", "") 143 # Use Binance USDT pair for better TradingView coverage 144 return (f"BINANCE:{base}USDT", "crypto") 145 146 def __repr__(self) -> str: 147 return f"Crypto('{self._pair}')"
A yfinance-like interface for cryptocurrency data from BtcTurk.
Examples:
import borsapy as bp btc = bp.Crypto("BTCTRY") btc.current {'symbol': 'BTCTRY', 'last': 3500000.0, ...} btc.history(period="1mo") Open High Low Close Volume Date 2024-12-01 3400000.0 3550000.0 3380000.0 3500000.0 1234.5678 ...
>>> eth = bp.Crypto("ETHTRY") >>> eth.current['last'] 125000.0
33 def __init__(self, pair: str): 34 """ 35 Initialize a Crypto object. 36 37 Args: 38 pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). 39 Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY 40 """ 41 self._pair = pair.upper() 42 self._provider = get_btcturk_provider() 43 self._current_cache: dict[str, Any] | None = None
Initialize a Crypto object.
Args: pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY
50 @property 51 def symbol(self) -> str: 52 """Return the trading pair (alias).""" 53 return self._pair
Return the trading pair (alias).
55 @property 56 def current(self) -> dict[str, Any]: 57 """ 58 Get current ticker information. 59 60 Returns: 61 Dictionary with current market data: 62 - symbol: Trading pair 63 - last: Last traded price 64 - open: Opening price 65 - high: 24h high 66 - low: 24h low 67 - bid: Best bid price 68 - ask: Best ask price 69 - volume: 24h volume 70 - change: Price change 71 - change_percent: Percent change 72 """ 73 if self._current_cache is None: 74 self._current_cache = self._provider.get_ticker(self._pair) 75 return self._current_cache
Get current ticker information.
Returns: Dictionary with current market data: - symbol: Trading pair - last: Last traded price - open: Opening price - high: 24h high - low: 24h low - bid: Best bid price - ask: Best ask price - volume: 24h volume - change: Price change - change_percent: Percent change
77 @property 78 def info(self) -> dict[str, Any]: 79 """Alias for current property (yfinance compatibility).""" 80 return self.current
Alias for current property (yfinance compatibility).
82 def history( 83 self, 84 period: str = "1mo", 85 interval: str = "1d", 86 start: datetime | str | None = None, 87 end: datetime | str | None = None, 88 ) -> pd.DataFrame: 89 """ 90 Get historical OHLCV data. 91 92 Args: 93 period: How much data to fetch. Valid periods: 94 1d, 5d, 1mo, 3mo, 6mo, 1y. 95 Ignored if start is provided. 96 interval: Data granularity. Valid intervals: 97 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. 98 start: Start date (string or datetime). 99 end: End date (string or datetime). Defaults to now. 100 101 Returns: 102 DataFrame with columns: Open, High, Low, Close, Volume. 103 Index is the Date. 104 105 Examples: 106 >>> crypto = Crypto("BTCTRY") 107 >>> crypto.history(period="1mo") # Last month 108 >>> crypto.history(period="1y", interval="1wk") # Weekly for 1 year 109 >>> crypto.history(start="2024-01-01", end="2024-06-30") # Date range 110 """ 111 start_dt = self._parse_date(start) if start else None 112 end_dt = self._parse_date(end) if end else None 113 114 return self._provider.get_history( 115 pair=self._pair, 116 period=period, 117 interval=interval, 118 start=start_dt, 119 end=end_dt, 120 )
Get historical OHLCV data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. interval: Data granularity. Valid intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. start: Start date (string or datetime). end: End date (string or datetime). Defaults to now.
Returns: DataFrame with columns: Open, High, Low, Close, Volume. Index is the Date.
Examples:
crypto = Crypto("BTCTRY") crypto.history(period="1mo") # Last month crypto.history(period="1y", interval="1wk") # Weekly for 1 year crypto.history(start="2024-01-01", end="2024-06-30") # Date range
15class Fund(TechnicalMixin): 16 """ 17 A yfinance-like interface for mutual fund data from TEFAS. 18 19 Examples: 20 >>> import borsapy as bp 21 >>> fund = bp.Fund("AAK") 22 >>> fund.info 23 {'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...} 24 >>> fund.history(period="1mo") 25 Price FundSize Investors 26 Date 27 2024-12-01 1.200 150000000.0 5000 28 ... 29 30 >>> fund = bp.Fund("TTE") 31 >>> fund.info['return_1y'] 32 45.67 33 """ 34 35 def __init__(self, fund_code: str, fund_type: str | None = None): 36 """ 37 Initialize a Fund object. 38 39 Args: 40 fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") 41 fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. 42 If None, auto-detects by trying YAT first, then EMK. 43 44 Examples: 45 >>> fund = bp.Fund("AAK") # Investment fund (auto-detect) 46 >>> fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit) 47 """ 48 self._fund_code = fund_code.upper() 49 self._fund_type = fund_type.upper() if fund_type else None 50 self._provider = get_tefas_provider() 51 self._info_cache: dict[str, Any] | None = None 52 self._detected_fund_type: str | None = None 53 54 @property 55 def fund_code(self) -> str: 56 """Return the fund code.""" 57 return self._fund_code 58 59 @property 60 def symbol(self) -> str: 61 """Return the fund code (alias).""" 62 return self._fund_code 63 64 @property 65 def fund_type(self) -> str: 66 """ 67 Return the fund type ("YAT" or "EMK"). 68 69 If not explicitly set, auto-detects on first history() or allocation() call. 70 """ 71 if self._fund_type: 72 return self._fund_type 73 if self._detected_fund_type: 74 return self._detected_fund_type 75 76 # Auto-detect by trying history with YAT first, then EMK 77 self._detect_fund_type() 78 return self._detected_fund_type or "YAT" 79 80 def _detect_fund_type(self) -> None: 81 """Auto-detect fund type by trying history API with different fund types.""" 82 if self._fund_type or self._detected_fund_type: 83 return 84 85 from datetime import timedelta 86 87 end_dt = datetime.now() 88 start_dt = end_dt - timedelta(days=7) 89 90 # Try YAT first 91 try: 92 df = self._provider._fetch_history_chunk( 93 self._fund_code, start_dt, end_dt, fund_type="YAT" 94 ) 95 if not df.empty: 96 self._detected_fund_type = "YAT" 97 return 98 except DataNotAvailableError: 99 pass 100 101 # Try EMK 102 try: 103 df = self._provider._fetch_history_chunk( 104 self._fund_code, start_dt, end_dt, fund_type="EMK" 105 ) 106 if not df.empty: 107 self._detected_fund_type = "EMK" 108 return 109 except DataNotAvailableError: 110 pass 111 112 # Default to YAT if neither works 113 self._detected_fund_type = "YAT" 114 115 @property 116 def info(self) -> dict[str, Any]: 117 """ 118 Get detailed fund information. 119 120 Returns: 121 Dictionary with fund details: 122 - fund_code: TEFAS fund code 123 - name: Fund full name 124 - date: Last update date 125 - price: Current unit price 126 - fund_size: Total fund size (TRY) 127 - investor_count: Number of investors 128 - founder: Fund founder company 129 - manager: Fund manager company 130 - fund_type: Fund type 131 - category: Fund category 132 - risk_value: Risk rating (1-7) 133 - return_1m, return_3m, return_6m: Period returns 134 - return_ytd: Year-to-date return 135 - return_1y, return_3y, return_5y: Annual returns 136 - daily_return: Daily return 137 """ 138 if self._info_cache is None: 139 # GetAllFundAnalyzeData works for both YAT and EMK without fontip 140 self._info_cache = self._provider.get_fund_detail(self._fund_code) 141 142 # If fund_type not explicitly set, we need to detect it for history/allocation 143 if not self._fund_type and not self._detected_fund_type: 144 # Detection will happen on first history() call 145 pass 146 147 return self._info_cache 148 149 @property 150 def detail(self) -> dict[str, Any]: 151 """Alias for info property.""" 152 return self.info 153 154 @property 155 def performance(self) -> dict[str, Any]: 156 """ 157 Get fund performance metrics only. 158 159 Returns: 160 Dictionary with performance data: 161 - daily_return: Daily return 162 - return_1m, return_3m, return_6m: Period returns 163 - return_ytd: Year-to-date return 164 - return_1y, return_3y, return_5y: Annual returns 165 """ 166 info = self.info 167 return { 168 "daily_return": info.get("daily_return"), 169 "return_1m": info.get("return_1m"), 170 "return_3m": info.get("return_3m"), 171 "return_6m": info.get("return_6m"), 172 "return_ytd": info.get("return_ytd"), 173 "return_1y": info.get("return_1y"), 174 "return_3y": info.get("return_3y"), 175 "return_5y": info.get("return_5y"), 176 } 177 178 @property 179 def allocation(self) -> pd.DataFrame: 180 """ 181 Get current portfolio allocation (asset breakdown) for last 7 days. 182 183 For longer periods, use allocation_history() method. 184 185 Returns: 186 DataFrame with columns: Date, asset_type, asset_name, weight. 187 188 Examples: 189 >>> fund = Fund("AAK") 190 >>> fund.allocation 191 Date asset_type asset_name weight 192 0 2024-12-20 HS Hisse Senedi 45.32 193 1 2024-12-20 DB Devlet Bonusu 30.15 194 ... 195 """ 196 return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type) 197 198 def allocation_history( 199 self, 200 period: str = "1mo", 201 start: datetime | str | None = None, 202 end: datetime | str | None = None, 203 ) -> pd.DataFrame: 204 """ 205 Get historical portfolio allocation (asset breakdown). 206 207 Note: TEFAS API supports maximum ~100 days (3 months) of data. 208 209 Args: 210 period: How much data to fetch. Valid periods: 211 1d, 5d, 1mo, 3mo (max ~100 days). 212 Ignored if start is provided. 213 start: Start date (string or datetime). 214 end: End date (string or datetime). Defaults to today. 215 216 Returns: 217 DataFrame with columns: Date, asset_type, asset_name, weight. 218 219 Examples: 220 >>> fund = Fund("AAK") 221 >>> fund.allocation_history(period="1mo") # Last month 222 >>> fund.allocation_history(period="3mo") # Last 3 months (max) 223 >>> fund.allocation_history(start="2024-10-01", end="2024-12-31") 224 """ 225 start_dt = self._parse_date(start) if start else None 226 end_dt = self._parse_date(end) if end else None 227 228 # If no start date, calculate from period 229 if start_dt is None: 230 from datetime import timedelta 231 end_dt = end_dt or datetime.now() 232 days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30) 233 # Cap at 100 days (API limit) 234 days = min(days, 100) 235 start_dt = end_dt - timedelta(days=days) 236 237 return self._provider.get_allocation( 238 fund_code=self._fund_code, 239 start=start_dt, 240 end=end_dt, 241 fund_type=self.fund_type, 242 ) 243 244 def history( 245 self, 246 period: str = "1mo", 247 start: datetime | str | None = None, 248 end: datetime | str | None = None, 249 ) -> pd.DataFrame: 250 """ 251 Get historical price data. 252 253 Args: 254 period: How much data to fetch. Valid periods: 255 1d, 5d, 1mo, 3mo, 6mo, 1y. 256 Ignored if start is provided. 257 start: Start date (string or datetime). 258 end: End date (string or datetime). Defaults to now. 259 260 Returns: 261 DataFrame with columns: Price, FundSize, Investors. 262 Index is the Date. 263 264 Examples: 265 >>> fund = Fund("AAK") 266 >>> fund.history(period="1mo") # Last month 267 >>> fund.history(period="1y") # Last year 268 >>> fund.history(start="2024-01-01", end="2024-06-30") # Date range 269 """ 270 start_dt = self._parse_date(start) if start else None 271 end_dt = self._parse_date(end) if end else None 272 273 return self._provider.get_history( 274 fund_code=self._fund_code, 275 period=period, 276 start=start_dt, 277 end=end_dt, 278 fund_type=self.fund_type, 279 ) 280 281 def _parse_date(self, date: str | datetime) -> datetime: 282 """Parse a date string to datetime.""" 283 if isinstance(date, datetime): 284 return date 285 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 286 try: 287 return datetime.strptime(date, fmt) 288 except ValueError: 289 continue 290 raise ValueError(f"Could not parse date: {date}") 291 292 def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float: 293 """ 294 Calculate the Sharpe ratio for the fund. 295 296 Sharpe Ratio = (Rp - Rf) / σp 297 Where: 298 - Rp = Annualized return of the fund 299 - Rf = Risk-free rate (default: 10Y government bond yield) 300 - σp = Annualized standard deviation of returns 301 302 Args: 303 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 304 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 305 If None, uses current 10Y bond yield from bp.risk_free_rate(). 306 307 Returns: 308 Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent). 309 310 Examples: 311 >>> fund = bp.Fund("YAY") 312 >>> fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 313 0.85 314 315 >>> fund.sharpe_ratio(period="3y") # 3-year Sharpe 316 1.23 317 318 >>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate 319 0.92 320 """ 321 metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate) 322 return metrics.get("sharpe_ratio", np.nan) 323 324 def risk_metrics( 325 self, 326 period: str = "1y", 327 risk_free_rate: float | None = None, 328 ) -> dict[str, Any]: 329 """ 330 Calculate comprehensive risk metrics for the fund. 331 332 Args: 333 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 334 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 335 If None, uses current 10Y bond yield. 336 337 Returns: 338 Dictionary with risk metrics: 339 - annualized_return: Annualized return (%) 340 - annualized_volatility: Annualized standard deviation (%) 341 - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp 342 - sortino_ratio: Downside risk-adjusted return 343 - max_drawdown: Maximum peak-to-trough decline (%) 344 - risk_free_rate: Risk-free rate used (%) 345 - trading_days: Number of trading days in the period 346 347 Examples: 348 >>> fund = bp.Fund("YAY") 349 >>> metrics = fund.risk_metrics() 350 >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") 351 >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%") 352 """ 353 # Get historical data 354 df = self.history(period=period) 355 356 if df.empty or len(df) < 20: 357 return { 358 "annualized_return": np.nan, 359 "annualized_volatility": np.nan, 360 "sharpe_ratio": np.nan, 361 "sortino_ratio": np.nan, 362 "max_drawdown": np.nan, 363 "risk_free_rate": np.nan, 364 "trading_days": 0, 365 } 366 367 # Calculate daily returns 368 prices = df["Price"] 369 daily_returns = prices.pct_change().dropna() 370 trading_days = len(daily_returns) 371 372 # Annualization factor (trading days per year) 373 annualization_factor = 252 374 375 # Annualized return 376 total_return = (prices.iloc[-1] / prices.iloc[0]) - 1 377 years = trading_days / annualization_factor 378 annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100 379 380 # Annualized volatility 381 daily_volatility = daily_returns.std() 382 annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100 383 384 # Get risk-free rate 385 if risk_free_rate is None: 386 try: 387 from borsapy.bond import risk_free_rate as get_rf_rate 388 rf = get_rf_rate() * 100 # Returns decimal like 0.28, convert to % 389 except Exception: 390 rf = 30.0 # Fallback: approximate Turkish 10Y yield 391 else: 392 rf = risk_free_rate * 100 # Convert decimal to percentage 393 394 # Sharpe Ratio 395 if annualized_volatility > 0: 396 sharpe = (annualized_return - rf) / annualized_volatility 397 else: 398 sharpe = np.nan 399 400 # Sortino Ratio (uses downside deviation) 401 negative_returns = daily_returns[daily_returns < 0] 402 if len(negative_returns) > 0: 403 downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100 404 if downside_deviation > 0: 405 sortino = (annualized_return - rf) / downside_deviation 406 else: 407 sortino = np.nan 408 else: 409 sortino = np.inf # No negative returns 410 411 # Maximum Drawdown 412 cumulative = (1 + daily_returns).cumprod() 413 running_max = cumulative.cummax() 414 drawdowns = (cumulative - running_max) / running_max 415 max_drawdown = drawdowns.min() * 100 # Negative percentage 416 417 return { 418 "annualized_return": round(annualized_return, 2), 419 "annualized_volatility": round(annualized_volatility, 2), 420 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 421 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 422 "max_drawdown": round(max_drawdown, 2), 423 "risk_free_rate": round(rf, 2), 424 "trading_days": trading_days, 425 } 426 427 def get_holdings( 428 self, 429 api_key: str, 430 period: str | None = None, 431 ) -> pd.DataFrame: 432 """ 433 Get detailed portfolio holdings (individual securities). 434 435 Returns the specific stocks, ETFs, and funds held by this fund, 436 with their weights and ISIN codes. Data is sourced from KAP 437 "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures. 438 439 Uses OpenRouter LLM for PDF parsing. 440 441 Args: 442 api_key: OpenRouter API key for LLM parsing. 443 Get your free API key at: https://openrouter.ai/ 444 period: Optional period in format "YYYY-MM" (e.g., "2025-12"). 445 If None, returns the most recent holdings. 446 447 Returns: 448 DataFrame with columns: 449 - symbol: Security symbol (e.g., "GOOGL", "THYAO") 450 - isin: ISIN code 451 - name: Full security name 452 - weight: Portfolio weight (%) 453 - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) 454 - country: Country ('TR', 'US', or None) 455 - value: Market value in TRY 456 457 Raises: 458 DataNotAvailableError: If holdings data not available. 459 APIError: If LLM parsing fails. 460 ImportError: If required packages are not installed. 461 462 Examples: 463 >>> fund = bp.Fund("YAY") 464 >>> fund.get_holdings(api_key="sk-or-v1-...") 465 symbol isin name weight type country value 466 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 467 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 468 ... 469 470 >>> # Get holdings for specific period 471 >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12") 472 473 >>> # Filter by type 474 >>> holdings = fund.get_holdings(api_key="sk-or-v1-...") 475 >>> holdings[holdings['type'] == 'stock'] 476 """ 477 from borsapy._providers.kap_holdings import get_kap_holdings_provider 478 479 provider = get_kap_holdings_provider() 480 return provider.get_holdings_df(self._fund_code, api_key, period=period) 481 482 def __repr__(self) -> str: 483 return f"Fund('{self._fund_code}')"
A yfinance-like interface for mutual fund data from TEFAS.
Examples:
import borsapy as bp fund = bp.Fund("AAK") fund.info {'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...} fund.history(period="1mo") Price FundSize Investors Date 2024-12-01 1.200 150000000.0 5000 ...
>>> fund = bp.Fund("TTE") >>> fund.info['return_1y'] 45.67
35 def __init__(self, fund_code: str, fund_type: str | None = None): 36 """ 37 Initialize a Fund object. 38 39 Args: 40 fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") 41 fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. 42 If None, auto-detects by trying YAT first, then EMK. 43 44 Examples: 45 >>> fund = bp.Fund("AAK") # Investment fund (auto-detect) 46 >>> fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit) 47 """ 48 self._fund_code = fund_code.upper() 49 self._fund_type = fund_type.upper() if fund_type else None 50 self._provider = get_tefas_provider() 51 self._info_cache: dict[str, Any] | None = None 52 self._detected_fund_type: str | None = None
Initialize a Fund object.
Args: fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. If None, auto-detects by trying YAT first, then EMK.
Examples:
fund = bp.Fund("AAK") # Investment fund (auto-detect) fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit)
54 @property 55 def fund_code(self) -> str: 56 """Return the fund code.""" 57 return self._fund_code
Return the fund code.
59 @property 60 def symbol(self) -> str: 61 """Return the fund code (alias).""" 62 return self._fund_code
Return the fund code (alias).
64 @property 65 def fund_type(self) -> str: 66 """ 67 Return the fund type ("YAT" or "EMK"). 68 69 If not explicitly set, auto-detects on first history() or allocation() call. 70 """ 71 if self._fund_type: 72 return self._fund_type 73 if self._detected_fund_type: 74 return self._detected_fund_type 75 76 # Auto-detect by trying history with YAT first, then EMK 77 self._detect_fund_type() 78 return self._detected_fund_type or "YAT"
Return the fund type ("YAT" or "EMK").
If not explicitly set, auto-detects on first history() or allocation() call.
115 @property 116 def info(self) -> dict[str, Any]: 117 """ 118 Get detailed fund information. 119 120 Returns: 121 Dictionary with fund details: 122 - fund_code: TEFAS fund code 123 - name: Fund full name 124 - date: Last update date 125 - price: Current unit price 126 - fund_size: Total fund size (TRY) 127 - investor_count: Number of investors 128 - founder: Fund founder company 129 - manager: Fund manager company 130 - fund_type: Fund type 131 - category: Fund category 132 - risk_value: Risk rating (1-7) 133 - return_1m, return_3m, return_6m: Period returns 134 - return_ytd: Year-to-date return 135 - return_1y, return_3y, return_5y: Annual returns 136 - daily_return: Daily return 137 """ 138 if self._info_cache is None: 139 # GetAllFundAnalyzeData works for both YAT and EMK without fontip 140 self._info_cache = self._provider.get_fund_detail(self._fund_code) 141 142 # If fund_type not explicitly set, we need to detect it for history/allocation 143 if not self._fund_type and not self._detected_fund_type: 144 # Detection will happen on first history() call 145 pass 146 147 return self._info_cache
Get detailed fund information.
Returns: Dictionary with fund details: - fund_code: TEFAS fund code - name: Fund full name - date: Last update date - price: Current unit price - fund_size: Total fund size (TRY) - investor_count: Number of investors - founder: Fund founder company - manager: Fund manager company - fund_type: Fund type - category: Fund category - risk_value: Risk rating (1-7) - return_1m, return_3m, return_6m: Period returns - return_ytd: Year-to-date return - return_1y, return_3y, return_5y: Annual returns - daily_return: Daily return
149 @property 150 def detail(self) -> dict[str, Any]: 151 """Alias for info property.""" 152 return self.info
Alias for info property.
154 @property 155 def performance(self) -> dict[str, Any]: 156 """ 157 Get fund performance metrics only. 158 159 Returns: 160 Dictionary with performance data: 161 - daily_return: Daily return 162 - return_1m, return_3m, return_6m: Period returns 163 - return_ytd: Year-to-date return 164 - return_1y, return_3y, return_5y: Annual returns 165 """ 166 info = self.info 167 return { 168 "daily_return": info.get("daily_return"), 169 "return_1m": info.get("return_1m"), 170 "return_3m": info.get("return_3m"), 171 "return_6m": info.get("return_6m"), 172 "return_ytd": info.get("return_ytd"), 173 "return_1y": info.get("return_1y"), 174 "return_3y": info.get("return_3y"), 175 "return_5y": info.get("return_5y"), 176 }
Get fund performance metrics only.
Returns: Dictionary with performance data: - daily_return: Daily return - return_1m, return_3m, return_6m: Period returns - return_ytd: Year-to-date return - return_1y, return_3y, return_5y: Annual returns
178 @property 179 def allocation(self) -> pd.DataFrame: 180 """ 181 Get current portfolio allocation (asset breakdown) for last 7 days. 182 183 For longer periods, use allocation_history() method. 184 185 Returns: 186 DataFrame with columns: Date, asset_type, asset_name, weight. 187 188 Examples: 189 >>> fund = Fund("AAK") 190 >>> fund.allocation 191 Date asset_type asset_name weight 192 0 2024-12-20 HS Hisse Senedi 45.32 193 1 2024-12-20 DB Devlet Bonusu 30.15 194 ... 195 """ 196 return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type)
Get current portfolio allocation (asset breakdown) for last 7 days.
For longer periods, use allocation_history() method.
Returns: DataFrame with columns: Date, asset_type, asset_name, weight.
Examples:
fund = Fund("AAK") fund.allocation Date asset_type asset_name weight 0 2024-12-20 HS Hisse Senedi 45.32 1 2024-12-20 DB Devlet Bonusu 30.15 ...
198 def allocation_history( 199 self, 200 period: str = "1mo", 201 start: datetime | str | None = None, 202 end: datetime | str | None = None, 203 ) -> pd.DataFrame: 204 """ 205 Get historical portfolio allocation (asset breakdown). 206 207 Note: TEFAS API supports maximum ~100 days (3 months) of data. 208 209 Args: 210 period: How much data to fetch. Valid periods: 211 1d, 5d, 1mo, 3mo (max ~100 days). 212 Ignored if start is provided. 213 start: Start date (string or datetime). 214 end: End date (string or datetime). Defaults to today. 215 216 Returns: 217 DataFrame with columns: Date, asset_type, asset_name, weight. 218 219 Examples: 220 >>> fund = Fund("AAK") 221 >>> fund.allocation_history(period="1mo") # Last month 222 >>> fund.allocation_history(period="3mo") # Last 3 months (max) 223 >>> fund.allocation_history(start="2024-10-01", end="2024-12-31") 224 """ 225 start_dt = self._parse_date(start) if start else None 226 end_dt = self._parse_date(end) if end else None 227 228 # If no start date, calculate from period 229 if start_dt is None: 230 from datetime import timedelta 231 end_dt = end_dt or datetime.now() 232 days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30) 233 # Cap at 100 days (API limit) 234 days = min(days, 100) 235 start_dt = end_dt - timedelta(days=days) 236 237 return self._provider.get_allocation( 238 fund_code=self._fund_code, 239 start=start_dt, 240 end=end_dt, 241 fund_type=self.fund_type, 242 )
Get historical portfolio allocation (asset breakdown).
Note: TEFAS API supports maximum ~100 days (3 months) of data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo (max ~100 days). Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.
Returns: DataFrame with columns: Date, asset_type, asset_name, weight.
Examples:
fund = Fund("AAK") fund.allocation_history(period="1mo") # Last month fund.allocation_history(period="3mo") # Last 3 months (max) fund.allocation_history(start="2024-10-01", end="2024-12-31")
244 def history( 245 self, 246 period: str = "1mo", 247 start: datetime | str | None = None, 248 end: datetime | str | None = None, 249 ) -> pd.DataFrame: 250 """ 251 Get historical price data. 252 253 Args: 254 period: How much data to fetch. Valid periods: 255 1d, 5d, 1mo, 3mo, 6mo, 1y. 256 Ignored if start is provided. 257 start: Start date (string or datetime). 258 end: End date (string or datetime). Defaults to now. 259 260 Returns: 261 DataFrame with columns: Price, FundSize, Investors. 262 Index is the Date. 263 264 Examples: 265 >>> fund = Fund("AAK") 266 >>> fund.history(period="1mo") # Last month 267 >>> fund.history(period="1y") # Last year 268 >>> fund.history(start="2024-01-01", end="2024-06-30") # Date range 269 """ 270 start_dt = self._parse_date(start) if start else None 271 end_dt = self._parse_date(end) if end else None 272 273 return self._provider.get_history( 274 fund_code=self._fund_code, 275 period=period, 276 start=start_dt, 277 end=end_dt, 278 fund_type=self.fund_type, 279 )
Get historical price data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to now.
Returns: DataFrame with columns: Price, FundSize, Investors. Index is the Date.
Examples:
fund = Fund("AAK") fund.history(period="1mo") # Last month fund.history(period="1y") # Last year fund.history(start="2024-01-01", end="2024-06-30") # Date range
292 def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float: 293 """ 294 Calculate the Sharpe ratio for the fund. 295 296 Sharpe Ratio = (Rp - Rf) / σp 297 Where: 298 - Rp = Annualized return of the fund 299 - Rf = Risk-free rate (default: 10Y government bond yield) 300 - σp = Annualized standard deviation of returns 301 302 Args: 303 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 304 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 305 If None, uses current 10Y bond yield from bp.risk_free_rate(). 306 307 Returns: 308 Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent). 309 310 Examples: 311 >>> fund = bp.Fund("YAY") 312 >>> fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 313 0.85 314 315 >>> fund.sharpe_ratio(period="3y") # 3-year Sharpe 316 1.23 317 318 >>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate 319 0.92 320 """ 321 metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate) 322 return metrics.get("sharpe_ratio", np.nan)
Calculate the Sharpe ratio for the fund.
Sharpe Ratio = (Rp - Rf) / σp Where:
- Rp = Annualized return of the fund
- Rf = Risk-free rate (default: 10Y government bond yield)
- σp = Annualized standard deviation of returns
Args: period: Period for calculation ("1y", "3y", "5y"). Default is "1y". risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield from bp.risk_free_rate().
Returns: Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).
Examples:
fund = bp.Fund("YAY") fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 0.85
>>> fund.sharpe_ratio(period="3y") # 3-year Sharpe 1.23 >>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate 0.92
324 def risk_metrics( 325 self, 326 period: str = "1y", 327 risk_free_rate: float | None = None, 328 ) -> dict[str, Any]: 329 """ 330 Calculate comprehensive risk metrics for the fund. 331 332 Args: 333 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 334 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 335 If None, uses current 10Y bond yield. 336 337 Returns: 338 Dictionary with risk metrics: 339 - annualized_return: Annualized return (%) 340 - annualized_volatility: Annualized standard deviation (%) 341 - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp 342 - sortino_ratio: Downside risk-adjusted return 343 - max_drawdown: Maximum peak-to-trough decline (%) 344 - risk_free_rate: Risk-free rate used (%) 345 - trading_days: Number of trading days in the period 346 347 Examples: 348 >>> fund = bp.Fund("YAY") 349 >>> metrics = fund.risk_metrics() 350 >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") 351 >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%") 352 """ 353 # Get historical data 354 df = self.history(period=period) 355 356 if df.empty or len(df) < 20: 357 return { 358 "annualized_return": np.nan, 359 "annualized_volatility": np.nan, 360 "sharpe_ratio": np.nan, 361 "sortino_ratio": np.nan, 362 "max_drawdown": np.nan, 363 "risk_free_rate": np.nan, 364 "trading_days": 0, 365 } 366 367 # Calculate daily returns 368 prices = df["Price"] 369 daily_returns = prices.pct_change().dropna() 370 trading_days = len(daily_returns) 371 372 # Annualization factor (trading days per year) 373 annualization_factor = 252 374 375 # Annualized return 376 total_return = (prices.iloc[-1] / prices.iloc[0]) - 1 377 years = trading_days / annualization_factor 378 annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100 379 380 # Annualized volatility 381 daily_volatility = daily_returns.std() 382 annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100 383 384 # Get risk-free rate 385 if risk_free_rate is None: 386 try: 387 from borsapy.bond import risk_free_rate as get_rf_rate 388 rf = get_rf_rate() * 100 # Returns decimal like 0.28, convert to % 389 except Exception: 390 rf = 30.0 # Fallback: approximate Turkish 10Y yield 391 else: 392 rf = risk_free_rate * 100 # Convert decimal to percentage 393 394 # Sharpe Ratio 395 if annualized_volatility > 0: 396 sharpe = (annualized_return - rf) / annualized_volatility 397 else: 398 sharpe = np.nan 399 400 # Sortino Ratio (uses downside deviation) 401 negative_returns = daily_returns[daily_returns < 0] 402 if len(negative_returns) > 0: 403 downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100 404 if downside_deviation > 0: 405 sortino = (annualized_return - rf) / downside_deviation 406 else: 407 sortino = np.nan 408 else: 409 sortino = np.inf # No negative returns 410 411 # Maximum Drawdown 412 cumulative = (1 + daily_returns).cumprod() 413 running_max = cumulative.cummax() 414 drawdowns = (cumulative - running_max) / running_max 415 max_drawdown = drawdowns.min() * 100 # Negative percentage 416 417 return { 418 "annualized_return": round(annualized_return, 2), 419 "annualized_volatility": round(annualized_volatility, 2), 420 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 421 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 422 "max_drawdown": round(max_drawdown, 2), 423 "risk_free_rate": round(rf, 2), 424 "trading_days": trading_days, 425 }
Calculate comprehensive risk metrics for the fund.
Args: period: Period for calculation ("1y", "3y", "5y"). Default is "1y". risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield.
Returns: Dictionary with risk metrics: - annualized_return: Annualized return (%) - annualized_volatility: Annualized standard deviation (%) - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp - sortino_ratio: Downside risk-adjusted return - max_drawdown: Maximum peak-to-trough decline (%) - risk_free_rate: Risk-free rate used (%) - trading_days: Number of trading days in the period
Examples:
fund = bp.Fund("YAY") metrics = fund.risk_metrics() print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")
427 def get_holdings( 428 self, 429 api_key: str, 430 period: str | None = None, 431 ) -> pd.DataFrame: 432 """ 433 Get detailed portfolio holdings (individual securities). 434 435 Returns the specific stocks, ETFs, and funds held by this fund, 436 with their weights and ISIN codes. Data is sourced from KAP 437 "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures. 438 439 Uses OpenRouter LLM for PDF parsing. 440 441 Args: 442 api_key: OpenRouter API key for LLM parsing. 443 Get your free API key at: https://openrouter.ai/ 444 period: Optional period in format "YYYY-MM" (e.g., "2025-12"). 445 If None, returns the most recent holdings. 446 447 Returns: 448 DataFrame with columns: 449 - symbol: Security symbol (e.g., "GOOGL", "THYAO") 450 - isin: ISIN code 451 - name: Full security name 452 - weight: Portfolio weight (%) 453 - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) 454 - country: Country ('TR', 'US', or None) 455 - value: Market value in TRY 456 457 Raises: 458 DataNotAvailableError: If holdings data not available. 459 APIError: If LLM parsing fails. 460 ImportError: If required packages are not installed. 461 462 Examples: 463 >>> fund = bp.Fund("YAY") 464 >>> fund.get_holdings(api_key="sk-or-v1-...") 465 symbol isin name weight type country value 466 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 467 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 468 ... 469 470 >>> # Get holdings for specific period 471 >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12") 472 473 >>> # Filter by type 474 >>> holdings = fund.get_holdings(api_key="sk-or-v1-...") 475 >>> holdings[holdings['type'] == 'stock'] 476 """ 477 from borsapy._providers.kap_holdings import get_kap_holdings_provider 478 479 provider = get_kap_holdings_provider() 480 return provider.get_holdings_df(self._fund_code, api_key, period=period)
Get detailed portfolio holdings (individual securities).
Returns the specific stocks, ETFs, and funds held by this fund, with their weights and ISIN codes. Data is sourced from KAP "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures.
Uses OpenRouter LLM for PDF parsing.
Args: api_key: OpenRouter API key for LLM parsing. Get your free API key at: https://openrouter.ai/ period: Optional period in format "YYYY-MM" (e.g., "2025-12"). If None, returns the most recent holdings.
Returns: DataFrame with columns: - symbol: Security symbol (e.g., "GOOGL", "THYAO") - isin: ISIN code - name: Full security name - weight: Portfolio weight (%) - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) - country: Country ('TR', 'US', or None) - value: Market value in TRY
Raises: DataNotAvailableError: If holdings data not available. APIError: If LLM parsing fails. ImportError: If required packages are not installed.
Examples:
fund = bp.Fund("YAY") fund.get_holdings(api_key="sk-or-v1-...") symbol isin name weight type country value 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 ...
>>> # Get holdings for specific period >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12") >>> # Filter by type >>> holdings = fund.get_holdings(api_key="sk-or-v1-...") >>> holdings[holdings['type'] == 'stock']
120class Portfolio(TechnicalMixin): 121 """ 122 Multi-asset portfolio management with performance tracking and risk metrics. 123 124 Supports 4 asset types: 125 - stock: BIST stocks via Ticker class 126 - fx: Currencies, metals, commodities via FX class 127 - crypto: Cryptocurrencies via Crypto class 128 - fund: TEFAS mutual funds via Fund class 129 130 Examples: 131 >>> import borsapy as bp 132 >>> p = bp.Portfolio() 133 >>> p.add("THYAO", shares=100, cost=280) 134 >>> p.add("gram-altin", shares=5, asset_type="fx") 135 >>> p.add("YAY", shares=500, asset_type="fund") 136 >>> p.set_benchmark("XU100") 137 >>> print(p.holdings) 138 >>> print(f"Value: {p.value:,.2f} TL") 139 >>> print(f"Sharpe: {p.risk_metrics()['sharpe_ratio']:.2f}") 140 """ 141 142 def __init__(self, benchmark: str = "XU100"): 143 """ 144 Initialize an empty portfolio. 145 146 Args: 147 benchmark: Index symbol for beta/alpha calculations. 148 Default is XU100 (BIST 100). 149 """ 150 self._holdings: dict[str, Holding] = {} 151 self._asset_cache: dict[str, Ticker | FX | Crypto | Fund] = {} 152 self._benchmark = benchmark 153 154 # === Asset Management === 155 156 def add( 157 self, 158 symbol: str, 159 shares: float, 160 cost: float | None = None, 161 asset_type: str | None = None, 162 purchase_date: str | date | datetime | None = None, 163 ) -> "Portfolio": 164 """ 165 Add an asset to the portfolio. 166 167 Args: 168 symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) 169 shares: Number of shares/units. 170 cost: Cost per share/unit. If None, uses current price. 171 asset_type: Asset type override. Auto-detected if None. 172 Valid values: "stock", "fx", "crypto", "fund" 173 purchase_date: Date when the asset was purchased. 174 Accepts string (YYYY-MM-DD), date, or datetime. 175 If None, defaults to today. 176 177 Returns: 178 Self for method chaining. 179 180 Examples: 181 >>> p = Portfolio() 182 >>> p.add("THYAO", shares=100, cost=280) # Stock with cost 183 >>> p.add("GARAN", shares=200) # Stock at current price 184 >>> p.add("gram-altin", shares=5, asset_type="fx") # Metal 185 >>> p.add("YAY", shares=500, asset_type="fund") # Mutual fund 186 >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15") 187 """ 188 symbol = symbol.upper() if asset_type != "fx" else symbol 189 190 # Detect or validate asset type 191 if asset_type is None: 192 detected_type = _detect_asset_type(symbol) 193 else: 194 detected_type = asset_type # type: ignore 195 196 # Get current price if cost not provided 197 if cost is None: 198 asset = self._get_or_create_asset(symbol, detected_type) 199 cost = self._get_current_price(asset) 200 201 # Parse purchase_date 202 parsed_date: date | None = None 203 if purchase_date is not None: 204 if isinstance(purchase_date, str): 205 parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date() 206 elif isinstance(purchase_date, datetime): 207 parsed_date = purchase_date.date() 208 elif isinstance(purchase_date, date): 209 parsed_date = purchase_date 210 else: 211 parsed_date = date.today() 212 213 self._holdings[symbol] = Holding( 214 symbol=symbol, 215 shares=shares, 216 cost_per_share=cost, 217 asset_type=detected_type, 218 purchase_date=parsed_date, 219 ) 220 221 return self 222 223 def remove(self, symbol: str) -> "Portfolio": 224 """ 225 Remove an asset from the portfolio. 226 227 Args: 228 symbol: Asset symbol to remove. 229 230 Returns: 231 Self for method chaining. 232 """ 233 symbol_upper = symbol.upper() 234 235 # Try both original and uppercase 236 if symbol in self._holdings: 237 del self._holdings[symbol] 238 self._asset_cache.pop(symbol, None) 239 elif symbol_upper in self._holdings: 240 del self._holdings[symbol_upper] 241 self._asset_cache.pop(symbol_upper, None) 242 243 return self 244 245 def update( 246 self, 247 symbol: str, 248 shares: float | None = None, 249 cost: float | None = None, 250 ) -> "Portfolio": 251 """ 252 Update an existing holding. 253 254 Args: 255 symbol: Asset symbol. 256 shares: New share count. If None, keeps existing. 257 cost: New cost per share. If None, keeps existing. 258 259 Returns: 260 Self for method chaining. 261 """ 262 if symbol not in self._holdings: 263 symbol = symbol.upper() 264 if symbol not in self._holdings: 265 raise KeyError(f"Symbol {symbol} not in portfolio") 266 267 holding = self._holdings[symbol] 268 if shares is not None: 269 holding.shares = shares 270 if cost is not None: 271 holding.cost_per_share = cost 272 273 return self 274 275 def clear(self) -> "Portfolio": 276 """ 277 Remove all holdings from the portfolio. 278 279 Returns: 280 Self for method chaining. 281 """ 282 self._holdings.clear() 283 self._asset_cache.clear() 284 return self 285 286 def set_benchmark(self, index: str) -> "Portfolio": 287 """ 288 Set the benchmark index for beta/alpha calculations. 289 290 Args: 291 index: Index symbol (XU100, XU030, XK030, etc.) 292 293 Returns: 294 Self for method chaining. 295 """ 296 self._benchmark = index 297 return self 298 299 # === Properties === 300 301 @property 302 def holdings(self) -> pd.DataFrame: 303 """ 304 Get all holdings as a DataFrame. 305 306 Returns: 307 DataFrame with columns: 308 - symbol: Asset symbol 309 - shares: Number of shares 310 - cost: Cost per share 311 - current_price: Current price 312 - value: Current value (shares * price) 313 - weight: Portfolio weight (%) 314 - pnl: Profit/loss (TL) 315 - pnl_pct: Profit/loss (%) 316 - asset_type: Asset type 317 - purchase_date: Date when asset was purchased 318 - holding_days: Number of days since purchase 319 """ 320 if not self._holdings: 321 return pd.DataFrame( 322 columns=[ 323 "symbol", "shares", "cost", "current_price", 324 "value", "weight", "pnl", "pnl_pct", "asset_type", 325 "purchase_date", "holding_days" 326 ] 327 ) 328 329 rows = [] 330 total_value = self.value 331 today = date.today() 332 333 for symbol, holding in self._holdings.items(): 334 asset = self._get_or_create_asset(symbol, holding.asset_type) 335 current_price = self._get_current_price(asset) 336 value = holding.shares * current_price 337 cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0 338 pnl = value - cost_basis if cost_basis else 0 339 pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0 340 weight = (value / total_value * 100) if total_value else 0 341 342 # Calculate holding days 343 holding_days = None 344 if holding.purchase_date: 345 holding_days = (today - holding.purchase_date).days 346 347 rows.append({ 348 "symbol": symbol, 349 "shares": holding.shares, 350 "cost": holding.cost_per_share, 351 "current_price": current_price, 352 "value": value, 353 "weight": round(weight, 2), 354 "pnl": round(pnl, 2), 355 "pnl_pct": round(pnl_pct, 2), 356 "asset_type": holding.asset_type, 357 "purchase_date": holding.purchase_date, 358 "holding_days": holding_days, 359 }) 360 361 return pd.DataFrame(rows) 362 363 @property 364 def symbols(self) -> list[str]: 365 """Get list of symbols in portfolio.""" 366 return list(self._holdings.keys()) 367 368 @property 369 def value(self) -> float: 370 """Get total portfolio value in TL.""" 371 total = 0.0 372 for symbol, holding in self._holdings.items(): 373 asset = self._get_or_create_asset(symbol, holding.asset_type) 374 price = self._get_current_price(asset) 375 total += holding.shares * price 376 return total 377 378 @property 379 def cost(self) -> float: 380 """Get total portfolio cost basis in TL.""" 381 total = 0.0 382 for holding in self._holdings.values(): 383 if holding.cost_per_share: 384 total += holding.shares * holding.cost_per_share 385 return total 386 387 @property 388 def pnl(self) -> float: 389 """Get total profit/loss in TL.""" 390 return self.value - self.cost 391 392 @property 393 def pnl_pct(self) -> float: 394 """Get total profit/loss as percentage.""" 395 cost = self.cost 396 if cost == 0: 397 return 0.0 398 return (self.pnl / cost) * 100 399 400 @property 401 def weights(self) -> dict[str, float]: 402 """Get portfolio weights as dictionary.""" 403 total_value = self.value 404 if total_value == 0: 405 return {} 406 407 result = {} 408 for symbol, holding in self._holdings.items(): 409 asset = self._get_or_create_asset(symbol, holding.asset_type) 410 price = self._get_current_price(asset) 411 value = holding.shares * price 412 result[symbol] = round(value / total_value, 4) 413 return result 414 415 # === Performance === 416 417 def history(self, period: str = "1y") -> pd.DataFrame: 418 """ 419 Get historical portfolio value based on current holdings. 420 421 Note: Uses current share counts - does not track historical trades. 422 When purchase_date is set for a holding, only data from that date 423 onwards is included in the portfolio value calculation. 424 425 Args: 426 period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y). 427 428 Returns: 429 DataFrame with columns: Value, Daily_Return. 430 Index is Date. 431 """ 432 if not self._holdings: 433 return pd.DataFrame(columns=["Value", "Daily_Return"]) 434 435 all_prices = {} 436 for symbol, holding in self._holdings.items(): 437 asset = self._get_or_create_asset(symbol, holding.asset_type) 438 try: 439 hist = asset.history(period=period) 440 if hist.empty: 441 continue 442 443 # Filter by purchase_date if set 444 if holding.purchase_date: 445 # Handle both timezone-aware and timezone-naive indices 446 if hasattr(hist.index, 'tz') and hist.index.tz is not None: 447 hist = hist[hist.index.date >= holding.purchase_date] 448 else: 449 hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)] 450 451 if hist.empty: 452 continue 453 454 # Use Close for stocks/index, Price for funds 455 price_col = "Close" if "Close" in hist.columns else "Price" 456 all_prices[symbol] = hist[price_col] * holding.shares 457 except Exception: 458 continue 459 460 if not all_prices: 461 return pd.DataFrame(columns=["Value", "Daily_Return"]) 462 463 df = pd.DataFrame(all_prices) 464 df = df.dropna(how="all") 465 df["Value"] = df.sum(axis=1) 466 df["Daily_Return"] = df["Value"].pct_change() 467 return df[["Value", "Daily_Return"]] 468 469 @property 470 def performance(self) -> dict[str, float]: 471 """ 472 Get portfolio performance summary. 473 474 Returns: 475 Dictionary with: 476 - total_return: Total return (%) 477 - annualized_return: Annualized return (%) 478 - total_value: Current value (TL) 479 - total_cost: Total cost (TL) 480 - total_pnl: Profit/loss (TL) 481 """ 482 return { 483 "total_return": self.pnl_pct, 484 "annualized_return": np.nan, # Calculated in risk_metrics 485 "total_value": self.value, 486 "total_cost": self.cost, 487 "total_pnl": self.pnl, 488 } 489 490 # === Risk Metrics === 491 492 def risk_metrics( 493 self, 494 period: str = "1y", 495 risk_free_rate: float | None = None, 496 ) -> dict[str, Any]: 497 """ 498 Calculate comprehensive risk metrics. 499 500 Args: 501 period: Period for calculation (1y, 3mo, 6mo). 502 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 503 If None, uses current 10Y bond yield. 504 505 Returns: 506 Dictionary with: 507 - annualized_return: Annualized return (%) 508 - annualized_volatility: Annualized volatility (%) 509 - sharpe_ratio: Risk-adjusted return 510 - sortino_ratio: Downside risk-adjusted return 511 - max_drawdown: Maximum drawdown (%) 512 - beta: Beta vs benchmark 513 - alpha: Alpha vs benchmark (%) 514 - risk_free_rate: Risk-free rate used (%) 515 - trading_days: Number of trading days 516 """ 517 df = self.history(period=period) 518 519 if df.empty or len(df) < 20: 520 return { 521 "annualized_return": np.nan, 522 "annualized_volatility": np.nan, 523 "sharpe_ratio": np.nan, 524 "sortino_ratio": np.nan, 525 "max_drawdown": np.nan, 526 "beta": np.nan, 527 "alpha": np.nan, 528 "risk_free_rate": np.nan, 529 "trading_days": 0, 530 } 531 532 daily_returns = df["Daily_Return"].dropna() 533 trading_days = len(daily_returns) 534 annualization = 252 535 536 # Annualized return 537 total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1 538 years = trading_days / annualization 539 ann_return = ((1 + total_return) ** (1 / years) - 1) * 100 540 541 # Annualized volatility 542 daily_volatility = daily_returns.std() 543 ann_volatility = daily_volatility * np.sqrt(annualization) * 100 544 545 # Get risk-free rate 546 if risk_free_rate is None: 547 try: 548 from borsapy.bond import risk_free_rate as get_rf_rate 549 rf = get_rf_rate() * 100 # Convert to percentage 550 except Exception: 551 rf = 30.0 # Fallback 552 else: 553 rf = risk_free_rate * 100 554 555 # Sharpe Ratio 556 if ann_volatility > 0: 557 sharpe = (ann_return - rf) / ann_volatility 558 else: 559 sharpe = np.nan 560 561 # Sortino Ratio (downside deviation) 562 negative_returns = daily_returns[daily_returns < 0] 563 if len(negative_returns) > 0: 564 downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100 565 if downside_deviation > 0: 566 sortino = (ann_return - rf) / downside_deviation 567 else: 568 sortino = np.nan 569 else: 570 sortino = np.inf # No negative returns 571 572 # Maximum Drawdown 573 cumulative = (1 + daily_returns).cumprod() 574 running_max = cumulative.cummax() 575 drawdowns = (cumulative - running_max) / running_max 576 max_drawdown = drawdowns.min() * 100 577 578 # Beta and Alpha (vs benchmark) 579 beta = np.nan 580 alpha = np.nan 581 582 try: 583 bench = Index(self._benchmark) 584 bench_hist = bench.history(period=period) 585 if not bench_hist.empty: 586 bench_returns = bench_hist["Close"].pct_change().dropna() 587 588 # Align dates 589 common_dates = daily_returns.index.intersection(bench_returns.index) 590 if len(common_dates) >= 20: 591 port_ret = daily_returns.loc[common_dates] 592 bench_ret = bench_returns.loc[common_dates] 593 594 # Beta = Cov(Rp, Rm) / Var(Rm) 595 covariance = port_ret.cov(bench_ret) 596 variance = bench_ret.var() 597 if variance > 0: 598 beta = covariance / variance 599 600 # Alpha = Rp - Rf - Beta * (Rm - Rf) 601 bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1 602 bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100 603 alpha = ann_return - rf - beta * (bench_ann - rf) 604 except Exception: 605 pass 606 607 return { 608 "annualized_return": round(ann_return, 2), 609 "annualized_volatility": round(ann_volatility, 2), 610 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 611 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 612 "max_drawdown": round(max_drawdown, 2), 613 "beta": round(beta, 2) if not np.isnan(beta) else np.nan, 614 "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan, 615 "risk_free_rate": round(rf, 2), 616 "trading_days": trading_days, 617 } 618 619 def sharpe_ratio(self, period: str = "1y") -> float: 620 """ 621 Calculate Sharpe ratio. 622 623 Args: 624 period: Period for calculation. 625 626 Returns: 627 Sharpe ratio. 628 """ 629 return self.risk_metrics(period=period).get("sharpe_ratio", np.nan) 630 631 def sortino_ratio(self, period: str = "1y") -> float: 632 """ 633 Calculate Sortino ratio. 634 635 Args: 636 period: Period for calculation. 637 638 Returns: 639 Sortino ratio. 640 """ 641 return self.risk_metrics(period=period).get("sortino_ratio", np.nan) 642 643 def beta(self, benchmark: str | None = None, period: str = "1y") -> float: 644 """ 645 Calculate beta vs benchmark. 646 647 Args: 648 benchmark: Benchmark index. Uses portfolio default if None. 649 period: Period for calculation. 650 651 Returns: 652 Beta coefficient. 653 """ 654 if benchmark: 655 old_bench = self._benchmark 656 self._benchmark = benchmark 657 result = self.risk_metrics(period=period).get("beta", np.nan) 658 self._benchmark = old_bench 659 return result 660 return self.risk_metrics(period=period).get("beta", np.nan) 661 662 def correlation_matrix(self, period: str = "1y") -> pd.DataFrame: 663 """ 664 Calculate correlation matrix between holdings. 665 666 Args: 667 period: Period for calculation. 668 669 Returns: 670 DataFrame with correlation coefficients. 671 """ 672 if len(self._holdings) < 2: 673 return pd.DataFrame() 674 675 returns_dict = {} 676 for symbol, holding in self._holdings.items(): 677 try: 678 asset = self._get_or_create_asset(symbol, holding.asset_type) 679 hist = asset.history(period=period) 680 if hist.empty: 681 continue 682 price_col = "Close" if "Close" in hist.columns else "Price" 683 returns_dict[symbol] = hist[price_col].pct_change() 684 except Exception: 685 continue 686 687 if len(returns_dict) < 2: 688 return pd.DataFrame() 689 690 df = pd.DataFrame(returns_dict).dropna() 691 return df.corr() 692 693 # === Import/Export === 694 695 def to_dict(self) -> dict[str, Any]: 696 """ 697 Export portfolio to dictionary. 698 699 Returns: 700 Dictionary with portfolio data. 701 """ 702 return { 703 "benchmark": self._benchmark, 704 "holdings": [ 705 { 706 "symbol": h.symbol, 707 "shares": h.shares, 708 "cost_per_share": h.cost_per_share, 709 "asset_type": h.asset_type, 710 "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None, 711 } 712 for h in self._holdings.values() 713 ], 714 } 715 716 @classmethod 717 def from_dict(cls, data: dict[str, Any]) -> "Portfolio": 718 """ 719 Create portfolio from dictionary. 720 721 Args: 722 data: Dictionary with portfolio data. 723 724 Returns: 725 Portfolio instance. 726 """ 727 portfolio = cls(benchmark=data.get("benchmark", "XU100")) 728 for h in data.get("holdings", []): 729 # Parse purchase_date from ISO string 730 purchase_date = None 731 if h.get("purchase_date"): 732 purchase_date = date.fromisoformat(h["purchase_date"]) 733 734 portfolio.add( 735 symbol=h["symbol"], 736 shares=h["shares"], 737 cost=h.get("cost_per_share"), 738 asset_type=h.get("asset_type"), 739 purchase_date=purchase_date, 740 ) 741 return portfolio 742 743 # === Private Methods === 744 745 def _get_or_create_asset( 746 self, symbol: str, asset_type: AssetType 747 ) -> Ticker | FX | Crypto | Fund: 748 """Get or create asset instance from cache.""" 749 cache_key = f"{symbol}_{asset_type}" 750 if cache_key not in self._asset_cache: 751 self._asset_cache[cache_key] = _get_asset(symbol, asset_type) 752 return self._asset_cache[cache_key] 753 754 def _get_current_price(self, asset: Ticker | FX | Crypto | Fund) -> float: 755 """Get current price from asset.""" 756 try: 757 if isinstance(asset, Ticker): 758 return asset.fast_info.last_price or 0 759 elif isinstance(asset, Crypto): 760 return asset.fast_info.last_price or 0 761 elif isinstance(asset, FX): 762 current = asset.current 763 return current.get("last", 0) if current else 0 764 elif isinstance(asset, Fund): 765 info = asset.info 766 return info.get("price", 0) if info else 0 767 except Exception: 768 pass 769 return 0 770 771 def __repr__(self) -> str: 772 n = len(self._holdings) 773 value = self.value 774 return f"Portfolio({n} holdings, {value:,.2f} TL)" 775 776 def __len__(self) -> int: 777 return len(self._holdings)
Multi-asset portfolio management with performance tracking and risk metrics.
Supports 4 asset types:
- stock: BIST stocks via Ticker class
- fx: Currencies, metals, commodities via FX class
- crypto: Cryptocurrencies via Crypto class
- fund: TEFAS mutual funds via Fund class
Examples:
import borsapy as bp p = bp.Portfolio() p.add("THYAO", shares=100, cost=280) p.add("gram-altin", shares=5, asset_type="fx") p.add("YAY", shares=500, asset_type="fund") p.set_benchmark("XU100") print(p.holdings) print(f"Value: {p.value:,.2f} TL") print(f"Sharpe: {p.risk_metrics()['sharpe_ratio']:.2f}")
142 def __init__(self, benchmark: str = "XU100"): 143 """ 144 Initialize an empty portfolio. 145 146 Args: 147 benchmark: Index symbol for beta/alpha calculations. 148 Default is XU100 (BIST 100). 149 """ 150 self._holdings: dict[str, Holding] = {} 151 self._asset_cache: dict[str, Ticker | FX | Crypto | Fund] = {} 152 self._benchmark = benchmark
Initialize an empty portfolio.
Args: benchmark: Index symbol for beta/alpha calculations. Default is XU100 (BIST 100).
156 def add( 157 self, 158 symbol: str, 159 shares: float, 160 cost: float | None = None, 161 asset_type: str | None = None, 162 purchase_date: str | date | datetime | None = None, 163 ) -> "Portfolio": 164 """ 165 Add an asset to the portfolio. 166 167 Args: 168 symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) 169 shares: Number of shares/units. 170 cost: Cost per share/unit. If None, uses current price. 171 asset_type: Asset type override. Auto-detected if None. 172 Valid values: "stock", "fx", "crypto", "fund" 173 purchase_date: Date when the asset was purchased. 174 Accepts string (YYYY-MM-DD), date, or datetime. 175 If None, defaults to today. 176 177 Returns: 178 Self for method chaining. 179 180 Examples: 181 >>> p = Portfolio() 182 >>> p.add("THYAO", shares=100, cost=280) # Stock with cost 183 >>> p.add("GARAN", shares=200) # Stock at current price 184 >>> p.add("gram-altin", shares=5, asset_type="fx") # Metal 185 >>> p.add("YAY", shares=500, asset_type="fund") # Mutual fund 186 >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15") 187 """ 188 symbol = symbol.upper() if asset_type != "fx" else symbol 189 190 # Detect or validate asset type 191 if asset_type is None: 192 detected_type = _detect_asset_type(symbol) 193 else: 194 detected_type = asset_type # type: ignore 195 196 # Get current price if cost not provided 197 if cost is None: 198 asset = self._get_or_create_asset(symbol, detected_type) 199 cost = self._get_current_price(asset) 200 201 # Parse purchase_date 202 parsed_date: date | None = None 203 if purchase_date is not None: 204 if isinstance(purchase_date, str): 205 parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date() 206 elif isinstance(purchase_date, datetime): 207 parsed_date = purchase_date.date() 208 elif isinstance(purchase_date, date): 209 parsed_date = purchase_date 210 else: 211 parsed_date = date.today() 212 213 self._holdings[symbol] = Holding( 214 symbol=symbol, 215 shares=shares, 216 cost_per_share=cost, 217 asset_type=detected_type, 218 purchase_date=parsed_date, 219 ) 220 221 return self
Add an asset to the portfolio.
Args: symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) shares: Number of shares/units. cost: Cost per share/unit. If None, uses current price. asset_type: Asset type override. Auto-detected if None. Valid values: "stock", "fx", "crypto", "fund" purchase_date: Date when the asset was purchased. Accepts string (YYYY-MM-DD), date, or datetime. If None, defaults to today.
Returns: Self for method chaining.
Examples:
p = Portfolio() p.add("THYAO", shares=100, cost=280) # Stock with cost p.add("GARAN", shares=200) # Stock at current price p.add("gram-altin", shares=5, asset_type="fx") # Metal p.add("YAY", shares=500, asset_type="fund") # Mutual fund p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15")
223 def remove(self, symbol: str) -> "Portfolio": 224 """ 225 Remove an asset from the portfolio. 226 227 Args: 228 symbol: Asset symbol to remove. 229 230 Returns: 231 Self for method chaining. 232 """ 233 symbol_upper = symbol.upper() 234 235 # Try both original and uppercase 236 if symbol in self._holdings: 237 del self._holdings[symbol] 238 self._asset_cache.pop(symbol, None) 239 elif symbol_upper in self._holdings: 240 del self._holdings[symbol_upper] 241 self._asset_cache.pop(symbol_upper, None) 242 243 return self
Remove an asset from the portfolio.
Args: symbol: Asset symbol to remove.
Returns: Self for method chaining.
245 def update( 246 self, 247 symbol: str, 248 shares: float | None = None, 249 cost: float | None = None, 250 ) -> "Portfolio": 251 """ 252 Update an existing holding. 253 254 Args: 255 symbol: Asset symbol. 256 shares: New share count. If None, keeps existing. 257 cost: New cost per share. If None, keeps existing. 258 259 Returns: 260 Self for method chaining. 261 """ 262 if symbol not in self._holdings: 263 symbol = symbol.upper() 264 if symbol not in self._holdings: 265 raise KeyError(f"Symbol {symbol} not in portfolio") 266 267 holding = self._holdings[symbol] 268 if shares is not None: 269 holding.shares = shares 270 if cost is not None: 271 holding.cost_per_share = cost 272 273 return self
Update an existing holding.
Args: symbol: Asset symbol. shares: New share count. If None, keeps existing. cost: New cost per share. If None, keeps existing.
Returns: Self for method chaining.
275 def clear(self) -> "Portfolio": 276 """ 277 Remove all holdings from the portfolio. 278 279 Returns: 280 Self for method chaining. 281 """ 282 self._holdings.clear() 283 self._asset_cache.clear() 284 return self
Remove all holdings from the portfolio.
Returns: Self for method chaining.
286 def set_benchmark(self, index: str) -> "Portfolio": 287 """ 288 Set the benchmark index for beta/alpha calculations. 289 290 Args: 291 index: Index symbol (XU100, XU030, XK030, etc.) 292 293 Returns: 294 Self for method chaining. 295 """ 296 self._benchmark = index 297 return self
Set the benchmark index for beta/alpha calculations.
Args: index: Index symbol (XU100, XU030, XK030, etc.)
Returns: Self for method chaining.
301 @property 302 def holdings(self) -> pd.DataFrame: 303 """ 304 Get all holdings as a DataFrame. 305 306 Returns: 307 DataFrame with columns: 308 - symbol: Asset symbol 309 - shares: Number of shares 310 - cost: Cost per share 311 - current_price: Current price 312 - value: Current value (shares * price) 313 - weight: Portfolio weight (%) 314 - pnl: Profit/loss (TL) 315 - pnl_pct: Profit/loss (%) 316 - asset_type: Asset type 317 - purchase_date: Date when asset was purchased 318 - holding_days: Number of days since purchase 319 """ 320 if not self._holdings: 321 return pd.DataFrame( 322 columns=[ 323 "symbol", "shares", "cost", "current_price", 324 "value", "weight", "pnl", "pnl_pct", "asset_type", 325 "purchase_date", "holding_days" 326 ] 327 ) 328 329 rows = [] 330 total_value = self.value 331 today = date.today() 332 333 for symbol, holding in self._holdings.items(): 334 asset = self._get_or_create_asset(symbol, holding.asset_type) 335 current_price = self._get_current_price(asset) 336 value = holding.shares * current_price 337 cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0 338 pnl = value - cost_basis if cost_basis else 0 339 pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0 340 weight = (value / total_value * 100) if total_value else 0 341 342 # Calculate holding days 343 holding_days = None 344 if holding.purchase_date: 345 holding_days = (today - holding.purchase_date).days 346 347 rows.append({ 348 "symbol": symbol, 349 "shares": holding.shares, 350 "cost": holding.cost_per_share, 351 "current_price": current_price, 352 "value": value, 353 "weight": round(weight, 2), 354 "pnl": round(pnl, 2), 355 "pnl_pct": round(pnl_pct, 2), 356 "asset_type": holding.asset_type, 357 "purchase_date": holding.purchase_date, 358 "holding_days": holding_days, 359 }) 360 361 return pd.DataFrame(rows)
Get all holdings as a DataFrame.
Returns: DataFrame with columns: - symbol: Asset symbol - shares: Number of shares - cost: Cost per share - current_price: Current price - value: Current value (shares * price) - weight: Portfolio weight (%) - pnl: Profit/loss (TL) - pnl_pct: Profit/loss (%) - asset_type: Asset type - purchase_date: Date when asset was purchased - holding_days: Number of days since purchase
363 @property 364 def symbols(self) -> list[str]: 365 """Get list of symbols in portfolio.""" 366 return list(self._holdings.keys())
Get list of symbols in portfolio.
368 @property 369 def value(self) -> float: 370 """Get total portfolio value in TL.""" 371 total = 0.0 372 for symbol, holding in self._holdings.items(): 373 asset = self._get_or_create_asset(symbol, holding.asset_type) 374 price = self._get_current_price(asset) 375 total += holding.shares * price 376 return total
Get total portfolio value in TL.
378 @property 379 def cost(self) -> float: 380 """Get total portfolio cost basis in TL.""" 381 total = 0.0 382 for holding in self._holdings.values(): 383 if holding.cost_per_share: 384 total += holding.shares * holding.cost_per_share 385 return total
Get total portfolio cost basis in TL.
387 @property 388 def pnl(self) -> float: 389 """Get total profit/loss in TL.""" 390 return self.value - self.cost
Get total profit/loss in TL.
392 @property 393 def pnl_pct(self) -> float: 394 """Get total profit/loss as percentage.""" 395 cost = self.cost 396 if cost == 0: 397 return 0.0 398 return (self.pnl / cost) * 100
Get total profit/loss as percentage.
400 @property 401 def weights(self) -> dict[str, float]: 402 """Get portfolio weights as dictionary.""" 403 total_value = self.value 404 if total_value == 0: 405 return {} 406 407 result = {} 408 for symbol, holding in self._holdings.items(): 409 asset = self._get_or_create_asset(symbol, holding.asset_type) 410 price = self._get_current_price(asset) 411 value = holding.shares * price 412 result[symbol] = round(value / total_value, 4) 413 return result
Get portfolio weights as dictionary.
417 def history(self, period: str = "1y") -> pd.DataFrame: 418 """ 419 Get historical portfolio value based on current holdings. 420 421 Note: Uses current share counts - does not track historical trades. 422 When purchase_date is set for a holding, only data from that date 423 onwards is included in the portfolio value calculation. 424 425 Args: 426 period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y). 427 428 Returns: 429 DataFrame with columns: Value, Daily_Return. 430 Index is Date. 431 """ 432 if not self._holdings: 433 return pd.DataFrame(columns=["Value", "Daily_Return"]) 434 435 all_prices = {} 436 for symbol, holding in self._holdings.items(): 437 asset = self._get_or_create_asset(symbol, holding.asset_type) 438 try: 439 hist = asset.history(period=period) 440 if hist.empty: 441 continue 442 443 # Filter by purchase_date if set 444 if holding.purchase_date: 445 # Handle both timezone-aware and timezone-naive indices 446 if hasattr(hist.index, 'tz') and hist.index.tz is not None: 447 hist = hist[hist.index.date >= holding.purchase_date] 448 else: 449 hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)] 450 451 if hist.empty: 452 continue 453 454 # Use Close for stocks/index, Price for funds 455 price_col = "Close" if "Close" in hist.columns else "Price" 456 all_prices[symbol] = hist[price_col] * holding.shares 457 except Exception: 458 continue 459 460 if not all_prices: 461 return pd.DataFrame(columns=["Value", "Daily_Return"]) 462 463 df = pd.DataFrame(all_prices) 464 df = df.dropna(how="all") 465 df["Value"] = df.sum(axis=1) 466 df["Daily_Return"] = df["Value"].pct_change() 467 return df[["Value", "Daily_Return"]]
Get historical portfolio value based on current holdings.
Note: Uses current share counts - does not track historical trades. When purchase_date is set for a holding, only data from that date onwards is included in the portfolio value calculation.
Args: period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).
Returns: DataFrame with columns: Value, Daily_Return. Index is Date.
469 @property 470 def performance(self) -> dict[str, float]: 471 """ 472 Get portfolio performance summary. 473 474 Returns: 475 Dictionary with: 476 - total_return: Total return (%) 477 - annualized_return: Annualized return (%) 478 - total_value: Current value (TL) 479 - total_cost: Total cost (TL) 480 - total_pnl: Profit/loss (TL) 481 """ 482 return { 483 "total_return": self.pnl_pct, 484 "annualized_return": np.nan, # Calculated in risk_metrics 485 "total_value": self.value, 486 "total_cost": self.cost, 487 "total_pnl": self.pnl, 488 }
Get portfolio performance summary.
Returns: Dictionary with: - total_return: Total return (%) - annualized_return: Annualized return (%) - total_value: Current value (TL) - total_cost: Total cost (TL) - total_pnl: Profit/loss (TL)
492 def risk_metrics( 493 self, 494 period: str = "1y", 495 risk_free_rate: float | None = None, 496 ) -> dict[str, Any]: 497 """ 498 Calculate comprehensive risk metrics. 499 500 Args: 501 period: Period for calculation (1y, 3mo, 6mo). 502 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 503 If None, uses current 10Y bond yield. 504 505 Returns: 506 Dictionary with: 507 - annualized_return: Annualized return (%) 508 - annualized_volatility: Annualized volatility (%) 509 - sharpe_ratio: Risk-adjusted return 510 - sortino_ratio: Downside risk-adjusted return 511 - max_drawdown: Maximum drawdown (%) 512 - beta: Beta vs benchmark 513 - alpha: Alpha vs benchmark (%) 514 - risk_free_rate: Risk-free rate used (%) 515 - trading_days: Number of trading days 516 """ 517 df = self.history(period=period) 518 519 if df.empty or len(df) < 20: 520 return { 521 "annualized_return": np.nan, 522 "annualized_volatility": np.nan, 523 "sharpe_ratio": np.nan, 524 "sortino_ratio": np.nan, 525 "max_drawdown": np.nan, 526 "beta": np.nan, 527 "alpha": np.nan, 528 "risk_free_rate": np.nan, 529 "trading_days": 0, 530 } 531 532 daily_returns = df["Daily_Return"].dropna() 533 trading_days = len(daily_returns) 534 annualization = 252 535 536 # Annualized return 537 total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1 538 years = trading_days / annualization 539 ann_return = ((1 + total_return) ** (1 / years) - 1) * 100 540 541 # Annualized volatility 542 daily_volatility = daily_returns.std() 543 ann_volatility = daily_volatility * np.sqrt(annualization) * 100 544 545 # Get risk-free rate 546 if risk_free_rate is None: 547 try: 548 from borsapy.bond import risk_free_rate as get_rf_rate 549 rf = get_rf_rate() * 100 # Convert to percentage 550 except Exception: 551 rf = 30.0 # Fallback 552 else: 553 rf = risk_free_rate * 100 554 555 # Sharpe Ratio 556 if ann_volatility > 0: 557 sharpe = (ann_return - rf) / ann_volatility 558 else: 559 sharpe = np.nan 560 561 # Sortino Ratio (downside deviation) 562 negative_returns = daily_returns[daily_returns < 0] 563 if len(negative_returns) > 0: 564 downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100 565 if downside_deviation > 0: 566 sortino = (ann_return - rf) / downside_deviation 567 else: 568 sortino = np.nan 569 else: 570 sortino = np.inf # No negative returns 571 572 # Maximum Drawdown 573 cumulative = (1 + daily_returns).cumprod() 574 running_max = cumulative.cummax() 575 drawdowns = (cumulative - running_max) / running_max 576 max_drawdown = drawdowns.min() * 100 577 578 # Beta and Alpha (vs benchmark) 579 beta = np.nan 580 alpha = np.nan 581 582 try: 583 bench = Index(self._benchmark) 584 bench_hist = bench.history(period=period) 585 if not bench_hist.empty: 586 bench_returns = bench_hist["Close"].pct_change().dropna() 587 588 # Align dates 589 common_dates = daily_returns.index.intersection(bench_returns.index) 590 if len(common_dates) >= 20: 591 port_ret = daily_returns.loc[common_dates] 592 bench_ret = bench_returns.loc[common_dates] 593 594 # Beta = Cov(Rp, Rm) / Var(Rm) 595 covariance = port_ret.cov(bench_ret) 596 variance = bench_ret.var() 597 if variance > 0: 598 beta = covariance / variance 599 600 # Alpha = Rp - Rf - Beta * (Rm - Rf) 601 bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1 602 bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100 603 alpha = ann_return - rf - beta * (bench_ann - rf) 604 except Exception: 605 pass 606 607 return { 608 "annualized_return": round(ann_return, 2), 609 "annualized_volatility": round(ann_volatility, 2), 610 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 611 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 612 "max_drawdown": round(max_drawdown, 2), 613 "beta": round(beta, 2) if not np.isnan(beta) else np.nan, 614 "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan, 615 "risk_free_rate": round(rf, 2), 616 "trading_days": trading_days, 617 }
Calculate comprehensive risk metrics.
Args: period: Period for calculation (1y, 3mo, 6mo). risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield.
Returns: Dictionary with: - annualized_return: Annualized return (%) - annualized_volatility: Annualized volatility (%) - sharpe_ratio: Risk-adjusted return - sortino_ratio: Downside risk-adjusted return - max_drawdown: Maximum drawdown (%) - beta: Beta vs benchmark - alpha: Alpha vs benchmark (%) - risk_free_rate: Risk-free rate used (%) - trading_days: Number of trading days
619 def sharpe_ratio(self, period: str = "1y") -> float: 620 """ 621 Calculate Sharpe ratio. 622 623 Args: 624 period: Period for calculation. 625 626 Returns: 627 Sharpe ratio. 628 """ 629 return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)
Calculate Sharpe ratio.
Args: period: Period for calculation.
Returns: Sharpe ratio.
631 def sortino_ratio(self, period: str = "1y") -> float: 632 """ 633 Calculate Sortino ratio. 634 635 Args: 636 period: Period for calculation. 637 638 Returns: 639 Sortino ratio. 640 """ 641 return self.risk_metrics(period=period).get("sortino_ratio", np.nan)
Calculate Sortino ratio.
Args: period: Period for calculation.
Returns: Sortino ratio.
643 def beta(self, benchmark: str | None = None, period: str = "1y") -> float: 644 """ 645 Calculate beta vs benchmark. 646 647 Args: 648 benchmark: Benchmark index. Uses portfolio default if None. 649 period: Period for calculation. 650 651 Returns: 652 Beta coefficient. 653 """ 654 if benchmark: 655 old_bench = self._benchmark 656 self._benchmark = benchmark 657 result = self.risk_metrics(period=period).get("beta", np.nan) 658 self._benchmark = old_bench 659 return result 660 return self.risk_metrics(period=period).get("beta", np.nan)
Calculate beta vs benchmark.
Args: benchmark: Benchmark index. Uses portfolio default if None. period: Period for calculation.
Returns: Beta coefficient.
662 def correlation_matrix(self, period: str = "1y") -> pd.DataFrame: 663 """ 664 Calculate correlation matrix between holdings. 665 666 Args: 667 period: Period for calculation. 668 669 Returns: 670 DataFrame with correlation coefficients. 671 """ 672 if len(self._holdings) < 2: 673 return pd.DataFrame() 674 675 returns_dict = {} 676 for symbol, holding in self._holdings.items(): 677 try: 678 asset = self._get_or_create_asset(symbol, holding.asset_type) 679 hist = asset.history(period=period) 680 if hist.empty: 681 continue 682 price_col = "Close" if "Close" in hist.columns else "Price" 683 returns_dict[symbol] = hist[price_col].pct_change() 684 except Exception: 685 continue 686 687 if len(returns_dict) < 2: 688 return pd.DataFrame() 689 690 df = pd.DataFrame(returns_dict).dropna() 691 return df.corr()
Calculate correlation matrix between holdings.
Args: period: Period for calculation.
Returns: DataFrame with correlation coefficients.
695 def to_dict(self) -> dict[str, Any]: 696 """ 697 Export portfolio to dictionary. 698 699 Returns: 700 Dictionary with portfolio data. 701 """ 702 return { 703 "benchmark": self._benchmark, 704 "holdings": [ 705 { 706 "symbol": h.symbol, 707 "shares": h.shares, 708 "cost_per_share": h.cost_per_share, 709 "asset_type": h.asset_type, 710 "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None, 711 } 712 for h in self._holdings.values() 713 ], 714 }
Export portfolio to dictionary.
Returns: Dictionary with portfolio data.
716 @classmethod 717 def from_dict(cls, data: dict[str, Any]) -> "Portfolio": 718 """ 719 Create portfolio from dictionary. 720 721 Args: 722 data: Dictionary with portfolio data. 723 724 Returns: 725 Portfolio instance. 726 """ 727 portfolio = cls(benchmark=data.get("benchmark", "XU100")) 728 for h in data.get("holdings", []): 729 # Parse purchase_date from ISO string 730 purchase_date = None 731 if h.get("purchase_date"): 732 purchase_date = date.fromisoformat(h["purchase_date"]) 733 734 portfolio.add( 735 symbol=h["symbol"], 736 shares=h["shares"], 737 cost=h.get("cost_per_share"), 738 asset_type=h.get("asset_type"), 739 purchase_date=purchase_date, 740 ) 741 return portfolio
Create portfolio from dictionary.
Args: data: Dictionary with portfolio data.
Returns: Portfolio instance.
55class Index(TechnicalMixin): 56 """ 57 A yfinance-like interface for Turkish market indices. 58 59 Examples: 60 >>> import borsapy as bp 61 >>> xu100 = bp.Index("XU100") 62 >>> xu100.info 63 {'symbol': 'XU100', 'name': 'BIST 100', 'last': 9500.5, ...} 64 >>> xu100.history(period="1mo") 65 Open High Low Close Volume 66 Date 67 2024-12-01 9400.00 9550.00 9380.00 9500.50 1234567890 68 ... 69 70 # Available indices 71 >>> bp.indices() 72 ['XU100', 'XU050', 'XU030', 'XBANK', ...] 73 """ 74 75 def __init__(self, symbol: str): 76 """ 77 Initialize an Index object. 78 79 Args: 80 symbol: Index symbol (e.g., "XU100", "XU030", "XBANK"). 81 """ 82 self._symbol = symbol.upper() 83 self._tradingview = get_tradingview_provider() 84 self._bist_index = get_bist_index_provider() 85 self._info_cache: dict[str, Any] | None = None 86 self._components_cache: list[dict[str, Any]] | None = None 87 88 @property 89 def symbol(self) -> str: 90 """Return the index symbol.""" 91 return self._symbol 92 93 @property 94 def info(self) -> dict[str, Any]: 95 """ 96 Get current index information. 97 98 Returns: 99 Dictionary with index data: 100 - symbol: Index symbol 101 - name: Index full name 102 - last: Current value 103 - open: Opening value 104 - high: Day high 105 - low: Day low 106 - close: Previous close 107 - change: Value change 108 - change_percent: Percent change 109 - update_time: Last update timestamp 110 """ 111 if self._info_cache is None: 112 # Use TradingView API to get quote (same endpoint works for indices) 113 quote = self._tradingview.get_quote(self._symbol) 114 quote["name"] = INDICES.get(self._symbol, self._symbol) 115 quote["type"] = "index" 116 self._info_cache = quote 117 return self._info_cache 118 119 @property 120 def components(self) -> list[dict[str, Any]]: 121 """ 122 Get constituent stocks of this index. 123 124 Returns: 125 List of component dicts with 'symbol' and 'name' keys. 126 Empty list if index components are not available. 127 128 Examples: 129 >>> import borsapy as bp 130 >>> xu030 = bp.Index("XU030") 131 >>> xu030.components 132 [{'symbol': 'AKBNK', 'name': 'AKBANK'}, ...] 133 >>> len(xu030.components) 134 30 135 """ 136 if self._components_cache is None: 137 self._components_cache = self._bist_index.get_components(self._symbol) 138 return self._components_cache 139 140 @property 141 def component_symbols(self) -> list[str]: 142 """ 143 Get just the ticker symbols of constituent stocks. 144 145 Returns: 146 List of stock symbols. 147 148 Examples: 149 >>> import borsapy as bp 150 >>> xu030 = bp.Index("XU030") 151 >>> xu030.component_symbols 152 ['AKBNK', 'AKSA', 'AKSEN', ...] 153 """ 154 return [c["symbol"] for c in self.components] 155 156 def history( 157 self, 158 period: str = "1mo", 159 interval: str = "1d", 160 start: datetime | str | None = None, 161 end: datetime | str | None = None, 162 ) -> pd.DataFrame: 163 """ 164 Get historical index data. 165 166 Args: 167 period: How much data to fetch. Valid periods: 168 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, ytd, max. 169 Ignored if start is provided. 170 interval: Data interval. Valid intervals: 171 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 172 start: Start date (string or datetime). 173 end: End date (string or datetime). Defaults to today. 174 175 Returns: 176 DataFrame with columns: Open, High, Low, Close, Volume. 177 Index is the Date. 178 179 Examples: 180 >>> idx = Index("XU100") 181 >>> idx.history(period="1mo") # Last month 182 >>> idx.history(period="1y") # Last year 183 >>> idx.history(start="2024-01-01", end="2024-06-30") 184 """ 185 # Parse dates 186 start_dt = self._parse_date(start) if start else None 187 end_dt = self._parse_date(end) if end else None 188 189 # Use TradingView provider (same API works for indices) 190 return self._tradingview.get_history( 191 symbol=self._symbol, 192 period=period, 193 interval=interval, 194 start=start_dt, 195 end=end_dt, 196 ) 197 198 def _parse_date(self, date: str | datetime) -> datetime: 199 """Parse a date string to datetime.""" 200 if isinstance(date, datetime): 201 return date 202 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 203 try: 204 return datetime.strptime(date, fmt) 205 except ValueError: 206 continue 207 raise ValueError(f"Could not parse date: {date}") 208 209 def _get_ta_symbol_info(self) -> tuple[str, str]: 210 """Get TradingView symbol and screener for TA signals. 211 212 Returns: 213 Tuple of (tv_symbol, screener) for TradingView Scanner API. 214 """ 215 return (f"BIST:{self._symbol}", "turkey") 216 217 def scan( 218 self, 219 condition: str, 220 period: str = "3mo", 221 interval: str = "1d", 222 ) -> "pd.DataFrame": 223 """Scan index components for technical conditions. 224 225 Convenience method for scanning all stocks in this index. 226 227 Args: 228 condition: Condition string (e.g., "rsi < 30", "price > sma_50") 229 period: Historical data period for indicator calculation 230 interval: Data interval 231 232 Returns: 233 DataFrame with matching symbols and their data 234 235 Examples: 236 >>> import borsapy as bp 237 >>> xu030 = bp.Index("XU030") 238 >>> xu030.scan("rsi < 30") 239 >>> xu030.scan("price > sma_50 and rsi > 50") 240 >>> xu030.scan("sma_20 crosses_above sma_50") 241 """ 242 from borsapy.scanner import scan 243 244 return scan(self.component_symbols, condition, period, interval) 245 246 def __repr__(self) -> str: 247 return f"Index('{self._symbol}')"
A yfinance-like interface for Turkish market indices.
Examples:
import borsapy as bp xu100 = bp.Index("XU100") xu100.info {'symbol': 'XU100', 'name': 'BIST 100', 'last': 9500.5, ...} xu100.history(period="1mo") Open High Low Close Volume Date 2024-12-01 9400.00 9550.00 9380.00 9500.50 1234567890 ...
# Available indices >>> bp.indices() ['XU100', 'XU050', 'XU030', 'XBANK', ...]
75 def __init__(self, symbol: str): 76 """ 77 Initialize an Index object. 78 79 Args: 80 symbol: Index symbol (e.g., "XU100", "XU030", "XBANK"). 81 """ 82 self._symbol = symbol.upper() 83 self._tradingview = get_tradingview_provider() 84 self._bist_index = get_bist_index_provider() 85 self._info_cache: dict[str, Any] | None = None 86 self._components_cache: list[dict[str, Any]] | None = None
Initialize an Index object.
Args: symbol: Index symbol (e.g., "XU100", "XU030", "XBANK").
93 @property 94 def info(self) -> dict[str, Any]: 95 """ 96 Get current index information. 97 98 Returns: 99 Dictionary with index data: 100 - symbol: Index symbol 101 - name: Index full name 102 - last: Current value 103 - open: Opening value 104 - high: Day high 105 - low: Day low 106 - close: Previous close 107 - change: Value change 108 - change_percent: Percent change 109 - update_time: Last update timestamp 110 """ 111 if self._info_cache is None: 112 # Use TradingView API to get quote (same endpoint works for indices) 113 quote = self._tradingview.get_quote(self._symbol) 114 quote["name"] = INDICES.get(self._symbol, self._symbol) 115 quote["type"] = "index" 116 self._info_cache = quote 117 return self._info_cache
Get current index information.
Returns: Dictionary with index data: - symbol: Index symbol - name: Index full name - last: Current value - open: Opening value - high: Day high - low: Day low - close: Previous close - change: Value change - change_percent: Percent change - update_time: Last update timestamp
119 @property 120 def components(self) -> list[dict[str, Any]]: 121 """ 122 Get constituent stocks of this index. 123 124 Returns: 125 List of component dicts with 'symbol' and 'name' keys. 126 Empty list if index components are not available. 127 128 Examples: 129 >>> import borsapy as bp 130 >>> xu030 = bp.Index("XU030") 131 >>> xu030.components 132 [{'symbol': 'AKBNK', 'name': 'AKBANK'}, ...] 133 >>> len(xu030.components) 134 30 135 """ 136 if self._components_cache is None: 137 self._components_cache = self._bist_index.get_components(self._symbol) 138 return self._components_cache
Get constituent stocks of this index.
Returns: List of component dicts with 'symbol' and 'name' keys. Empty list if index components are not available.
Examples:
import borsapy as bp xu030 = bp.Index("XU030") xu030.components [{'symbol': 'AKBNK', 'name': 'AKBANK'}, ...] len(xu030.components) 30
140 @property 141 def component_symbols(self) -> list[str]: 142 """ 143 Get just the ticker symbols of constituent stocks. 144 145 Returns: 146 List of stock symbols. 147 148 Examples: 149 >>> import borsapy as bp 150 >>> xu030 = bp.Index("XU030") 151 >>> xu030.component_symbols 152 ['AKBNK', 'AKSA', 'AKSEN', ...] 153 """ 154 return [c["symbol"] for c in self.components]
Get just the ticker symbols of constituent stocks.
Returns: List of stock symbols.
Examples:
import borsapy as bp xu030 = bp.Index("XU030") xu030.component_symbols ['AKBNK', 'AKSA', 'AKSEN', ...]
156 def history( 157 self, 158 period: str = "1mo", 159 interval: str = "1d", 160 start: datetime | str | None = None, 161 end: datetime | str | None = None, 162 ) -> pd.DataFrame: 163 """ 164 Get historical index data. 165 166 Args: 167 period: How much data to fetch. Valid periods: 168 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, ytd, max. 169 Ignored if start is provided. 170 interval: Data interval. Valid intervals: 171 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 172 start: Start date (string or datetime). 173 end: End date (string or datetime). Defaults to today. 174 175 Returns: 176 DataFrame with columns: Open, High, Low, Close, Volume. 177 Index is the Date. 178 179 Examples: 180 >>> idx = Index("XU100") 181 >>> idx.history(period="1mo") # Last month 182 >>> idx.history(period="1y") # Last year 183 >>> idx.history(start="2024-01-01", end="2024-06-30") 184 """ 185 # Parse dates 186 start_dt = self._parse_date(start) if start else None 187 end_dt = self._parse_date(end) if end else None 188 189 # Use TradingView provider (same API works for indices) 190 return self._tradingview.get_history( 191 symbol=self._symbol, 192 period=period, 193 interval=interval, 194 start=start_dt, 195 end=end_dt, 196 )
Get historical index data.
Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, ytd, max. Ignored if start is provided. interval: Data interval. Valid intervals: 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.
Returns: DataFrame with columns: Open, High, Low, Close, Volume. Index is the Date.
Examples:
idx = Index("XU100") idx.history(period="1mo") # Last month idx.history(period="1y") # Last year idx.history(start="2024-01-01", end="2024-06-30")
217 def scan( 218 self, 219 condition: str, 220 period: str = "3mo", 221 interval: str = "1d", 222 ) -> "pd.DataFrame": 223 """Scan index components for technical conditions. 224 225 Convenience method for scanning all stocks in this index. 226 227 Args: 228 condition: Condition string (e.g., "rsi < 30", "price > sma_50") 229 period: Historical data period for indicator calculation 230 interval: Data interval 231 232 Returns: 233 DataFrame with matching symbols and their data 234 235 Examples: 236 >>> import borsapy as bp 237 >>> xu030 = bp.Index("XU030") 238 >>> xu030.scan("rsi < 30") 239 >>> xu030.scan("price > sma_50 and rsi > 50") 240 >>> xu030.scan("sma_20 crosses_above sma_50") 241 """ 242 from borsapy.scanner import scan 243 244 return scan(self.component_symbols, condition, period, interval)
Scan index components for technical conditions.
Convenience method for scanning all stocks in this index.
Args: condition: Condition string (e.g., "rsi < 30", "price > sma_50") period: Historical data period for indicator calculation interval: Data interval
Returns: DataFrame with matching symbols and their data
Examples:
import borsapy as bp xu030 = bp.Index("XU030") xu030.scan("rsi < 30") xu030.scan("price > sma_50 and rsi > 50") xu030.scan("sma_20 crosses_above sma_50")
11class Inflation: 12 """ 13 A yfinance-like interface for Turkish inflation data from TCMB. 14 15 Examples: 16 >>> import borsapy as bp 17 >>> inf = bp.Inflation() 18 19 # Get latest inflation 20 >>> inf.latest() 21 {'date': '2024-11-01', 'yearly_inflation': 47.09, 'monthly_inflation': 2.24, ...} 22 23 # Get TÜFE history 24 >>> inf.tufe(limit=12) # Last 12 months 25 YearMonth YearlyInflation MonthlyInflation 26 Date 27 2024-11-01 11-2024 47.09 2.24 28 ... 29 30 # Calculate inflation 31 >>> inf.calculate(100000, "2020-01", "2024-01") 32 {'initial_value': 100000, 'final_value': 342515.0, 'total_change': 242.52, ...} 33 """ 34 35 def __init__(self): 36 """Initialize an Inflation object.""" 37 self._provider = get_tcmb_provider() 38 39 def latest(self, inflation_type: str = "tufe") -> dict[str, Any]: 40 """ 41 Get the latest inflation data. 42 43 Args: 44 inflation_type: 'tufe' (CPI) or 'ufe' (PPI) 45 46 Returns: 47 Dictionary with latest inflation data: 48 - date: Date string (YYYY-MM-DD) 49 - year_month: Month-Year string 50 - yearly_inflation: Year-over-year inflation rate 51 - monthly_inflation: Month-over-month inflation rate 52 - type: Inflation type (TUFE or UFE) 53 """ 54 return self._provider.get_latest(inflation_type) 55 56 def tufe( 57 self, 58 start: str | None = None, 59 end: str | None = None, 60 limit: int | None = None, 61 ) -> pd.DataFrame: 62 """ 63 Get TÜFE (Consumer Price Index) data. 64 65 Args: 66 start: Start date in YYYY-MM-DD format 67 end: End date in YYYY-MM-DD format 68 limit: Maximum number of records 69 70 Returns: 71 DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. 72 Index is the Date. 73 74 Examples: 75 >>> inf = Inflation() 76 >>> inf.tufe(limit=6) # Last 6 months 77 >>> inf.tufe(start="2023-01-01", end="2023-12-31") # 2023 data 78 """ 79 return self._provider.get_data("tufe", start, end, limit) 80 81 def ufe( 82 self, 83 start: str | None = None, 84 end: str | None = None, 85 limit: int | None = None, 86 ) -> pd.DataFrame: 87 """ 88 Get ÜFE (Producer Price Index) data. 89 90 Args: 91 start: Start date in YYYY-MM-DD format 92 end: End date in YYYY-MM-DD format 93 limit: Maximum number of records 94 95 Returns: 96 DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. 97 Index is the Date. 98 99 Examples: 100 >>> inf = Inflation() 101 >>> inf.ufe(limit=6) # Last 6 months 102 >>> inf.ufe(start="2023-01-01", end="2023-12-31") # 2023 data 103 """ 104 return self._provider.get_data("ufe", start, end, limit) 105 106 def calculate( 107 self, 108 amount: float, 109 start: str, 110 end: str, 111 ) -> dict[str, Any]: 112 """ 113 Calculate inflation-adjusted value between two dates. 114 115 Uses TCMB's official inflation calculator API. 116 117 Args: 118 amount: Initial amount in TRY 119 start: Start date in YYYY-MM format (e.g., "2020-01") 120 end: End date in YYYY-MM format (e.g., "2024-01") 121 122 Returns: 123 Dictionary with: 124 - start_date: Start date 125 - end_date: End date 126 - initial_value: Initial amount 127 - final_value: Inflation-adjusted value 128 - total_years: Total years elapsed 129 - total_months: Total months elapsed 130 - total_change: Total percentage change 131 - avg_yearly_inflation: Average yearly inflation rate 132 - start_cpi: CPI at start date 133 - end_cpi: CPI at end date 134 135 Examples: 136 >>> inf = Inflation() 137 >>> result = inf.calculate(100000, "2020-01", "2024-01") 138 >>> print(f"100,000 TL in 2020 = {result['final_value']:,.0f} TL in 2024") 139 100,000 TL in 2020 = 342,515 TL in 2024 140 """ 141 start_year, start_month = self._parse_year_month(start) 142 end_year, end_month = self._parse_year_month(end) 143 144 return self._provider.calculate_inflation( 145 start_year=start_year, 146 start_month=start_month, 147 end_year=end_year, 148 end_month=end_month, 149 basket_value=amount, 150 ) 151 152 def _parse_year_month(self, date_str: str) -> tuple[int, int]: 153 """Parse YYYY-MM format to (year, month) tuple.""" 154 try: 155 parts = date_str.split("-") 156 if len(parts) != 2: 157 raise ValueError(f"Invalid date format: {date_str}. Use YYYY-MM") 158 year = int(parts[0]) 159 month = int(parts[1]) 160 if not (1 <= month <= 12): 161 raise ValueError(f"Invalid month: {month}") 162 return year, month 163 except Exception as e: 164 raise ValueError(f"Could not parse date '{date_str}': {e}") from e 165 166 def __repr__(self) -> str: 167 return "Inflation()"
A yfinance-like interface for Turkish inflation data from TCMB.
Examples:
import borsapy as bp inf = bp.Inflation()
# Get latest inflation >>> inf.latest() {'date': '2024-11-01', 'yearly_inflation': 47.09, 'monthly_inflation': 2.24, ...} # Get TÜFE history >>> inf.tufe(limit=12) # Last 12 months YearMonth YearlyInflation MonthlyInflation Date 2024-11-01 11-2024 47.09 2.24 ... # Calculate inflation >>> inf.calculate(100000, "2020-01", "2024-01") {'initial_value': 100000, 'final_value': 342515.0, 'total_change': 242.52, ...}
35 def __init__(self): 36 """Initialize an Inflation object.""" 37 self._provider = get_tcmb_provider()
Initialize an Inflation object.
39 def latest(self, inflation_type: str = "tufe") -> dict[str, Any]: 40 """ 41 Get the latest inflation data. 42 43 Args: 44 inflation_type: 'tufe' (CPI) or 'ufe' (PPI) 45 46 Returns: 47 Dictionary with latest inflation data: 48 - date: Date string (YYYY-MM-DD) 49 - year_month: Month-Year string 50 - yearly_inflation: Year-over-year inflation rate 51 - monthly_inflation: Month-over-month inflation rate 52 - type: Inflation type (TUFE or UFE) 53 """ 54 return self._provider.get_latest(inflation_type)
Get the latest inflation data.
Args: inflation_type: 'tufe' (CPI) or 'ufe' (PPI)
Returns: Dictionary with latest inflation data: - date: Date string (YYYY-MM-DD) - year_month: Month-Year string - yearly_inflation: Year-over-year inflation rate - monthly_inflation: Month-over-month inflation rate - type: Inflation type (TUFE or UFE)
56 def tufe( 57 self, 58 start: str | None = None, 59 end: str | None = None, 60 limit: int | None = None, 61 ) -> pd.DataFrame: 62 """ 63 Get TÜFE (Consumer Price Index) data. 64 65 Args: 66 start: Start date in YYYY-MM-DD format 67 end: End date in YYYY-MM-DD format 68 limit: Maximum number of records 69 70 Returns: 71 DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. 72 Index is the Date. 73 74 Examples: 75 >>> inf = Inflation() 76 >>> inf.tufe(limit=6) # Last 6 months 77 >>> inf.tufe(start="2023-01-01", end="2023-12-31") # 2023 data 78 """ 79 return self._provider.get_data("tufe", start, end, limit)
Get TÜFE (Consumer Price Index) data.
Args: start: Start date in YYYY-MM-DD format end: End date in YYYY-MM-DD format limit: Maximum number of records
Returns: DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. Index is the Date.
Examples:
inf = Inflation() inf.tufe(limit=6) # Last 6 months inf.tufe(start="2023-01-01", end="2023-12-31") # 2023 data
81 def ufe( 82 self, 83 start: str | None = None, 84 end: str | None = None, 85 limit: int | None = None, 86 ) -> pd.DataFrame: 87 """ 88 Get ÜFE (Producer Price Index) data. 89 90 Args: 91 start: Start date in YYYY-MM-DD format 92 end: End date in YYYY-MM-DD format 93 limit: Maximum number of records 94 95 Returns: 96 DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. 97 Index is the Date. 98 99 Examples: 100 >>> inf = Inflation() 101 >>> inf.ufe(limit=6) # Last 6 months 102 >>> inf.ufe(start="2023-01-01", end="2023-12-31") # 2023 data 103 """ 104 return self._provider.get_data("ufe", start, end, limit)
Get ÜFE (Producer Price Index) data.
Args: start: Start date in YYYY-MM-DD format end: End date in YYYY-MM-DD format limit: Maximum number of records
Returns: DataFrame with columns: YearMonth, YearlyInflation, MonthlyInflation. Index is the Date.
Examples:
inf = Inflation() inf.ufe(limit=6) # Last 6 months inf.ufe(start="2023-01-01", end="2023-12-31") # 2023 data
106 def calculate( 107 self, 108 amount: float, 109 start: str, 110 end: str, 111 ) -> dict[str, Any]: 112 """ 113 Calculate inflation-adjusted value between two dates. 114 115 Uses TCMB's official inflation calculator API. 116 117 Args: 118 amount: Initial amount in TRY 119 start: Start date in YYYY-MM format (e.g., "2020-01") 120 end: End date in YYYY-MM format (e.g., "2024-01") 121 122 Returns: 123 Dictionary with: 124 - start_date: Start date 125 - end_date: End date 126 - initial_value: Initial amount 127 - final_value: Inflation-adjusted value 128 - total_years: Total years elapsed 129 - total_months: Total months elapsed 130 - total_change: Total percentage change 131 - avg_yearly_inflation: Average yearly inflation rate 132 - start_cpi: CPI at start date 133 - end_cpi: CPI at end date 134 135 Examples: 136 >>> inf = Inflation() 137 >>> result = inf.calculate(100000, "2020-01", "2024-01") 138 >>> print(f"100,000 TL in 2020 = {result['final_value']:,.0f} TL in 2024") 139 100,000 TL in 2020 = 342,515 TL in 2024 140 """ 141 start_year, start_month = self._parse_year_month(start) 142 end_year, end_month = self._parse_year_month(end) 143 144 return self._provider.calculate_inflation( 145 start_year=start_year, 146 start_month=start_month, 147 end_year=end_year, 148 end_month=end_month, 149 basket_value=amount, 150 )
Calculate inflation-adjusted value between two dates.
Uses TCMB's official inflation calculator API.
Args: amount: Initial amount in TRY start: Start date in YYYY-MM format (e.g., "2020-01") end: End date in YYYY-MM format (e.g., "2024-01")
Returns: Dictionary with: - start_date: Start date - end_date: End date - initial_value: Initial amount - final_value: Inflation-adjusted value - total_years: Total years elapsed - total_months: Total months elapsed - total_change: Total percentage change - avg_yearly_inflation: Average yearly inflation rate - start_cpi: CPI at start date - end_cpi: CPI at end date
Examples:
inf = Inflation() result = inf.calculate(100000, "2020-01", "2024-01") print(f"100,000 TL in 2020 = {result['final_value']:,.0f} TL in 2024") 100,000 TL in 2020 = 342,515 TL in 2024
13class VIOP: 14 """ 15 VİOP (Vadeli İşlem ve Opsiyon Piyasası) data access. 16 17 Provides access to Turkish derivatives market data including 18 futures and options contracts. 19 20 Data source: İş Yatırım (HTML scraping) 21 Note: Data is delayed by ~15 minutes 22 23 Examples: 24 >>> from borsapy import VIOP 25 >>> viop = VIOP() 26 >>> viop.futures # All futures contracts 27 >>> viop.stock_futures # Stock futures only 28 >>> viop.options # All options contracts 29 """ 30 31 def __init__(self) -> None: 32 """Initialize VİOP data accessor.""" 33 self._provider = get_viop_provider() 34 35 @cached_property 36 def futures(self) -> pd.DataFrame: 37 """ 38 Get all futures contracts. 39 40 Returns: 41 DataFrame with columns: 42 - code: Contract code (e.g., F_AKBNK0226) 43 - contract: Contract name (e.g., AKBNK Şubat 2026 Vadeli) 44 - price: Last price 45 - change: Price change 46 - volume_tl: Trading volume in TL 47 - volume_qty: Trading volume in contracts 48 - category: stock, index, currency, or commodity 49 """ 50 return self._provider.get_futures("all") 51 52 @cached_property 53 def stock_futures(self) -> pd.DataFrame: 54 """ 55 Get stock futures contracts (Pay Vadeli İşlem). 56 57 Returns: 58 DataFrame with futures on individual stocks. 59 """ 60 return self._provider.get_futures("stock") 61 62 @cached_property 63 def index_futures(self) -> pd.DataFrame: 64 """ 65 Get index futures contracts (Endeks Vadeli İşlem). 66 67 Includes XU030, XLBNK, etc. 68 69 Returns: 70 DataFrame with index futures. 71 """ 72 return self._provider.get_futures("index") 73 74 @cached_property 75 def currency_futures(self) -> pd.DataFrame: 76 """ 77 Get currency futures contracts (Döviz Vadeli İşlem). 78 79 Includes USD/TRY, EUR/TRY, etc. 80 81 Returns: 82 DataFrame with currency futures. 83 """ 84 return self._provider.get_futures("currency") 85 86 @cached_property 87 def commodity_futures(self) -> pd.DataFrame: 88 """ 89 Get commodity futures contracts (Kıymetli Madenler). 90 91 Includes gold, silver, platinum, palladium. 92 93 Returns: 94 DataFrame with commodity futures. 95 """ 96 return self._provider.get_futures("commodity") 97 98 @cached_property 99 def options(self) -> pd.DataFrame: 100 """ 101 Get all options contracts. 102 103 Returns: 104 DataFrame with columns: 105 - code: Contract code 106 - contract: Contract name 107 - price: Last price 108 - change: Price change 109 - volume_tl: Trading volume in TL 110 - volume_qty: Trading volume in contracts 111 - category: stock or index 112 """ 113 return self._provider.get_options("all") 114 115 @cached_property 116 def stock_options(self) -> pd.DataFrame: 117 """ 118 Get stock options contracts (Pay Opsiyon). 119 120 Returns: 121 DataFrame with options on individual stocks. 122 """ 123 return self._provider.get_options("stock") 124 125 @cached_property 126 def index_options(self) -> pd.DataFrame: 127 """ 128 Get index options contracts (Endeks Opsiyon). 129 130 Returns: 131 DataFrame with index options. 132 """ 133 return self._provider.get_options("index") 134 135 def get_by_symbol(self, symbol: str) -> pd.DataFrame: 136 """ 137 Get all derivatives for a specific underlying symbol. 138 139 Args: 140 symbol: Underlying symbol (e.g., "AKBNK", "THYAO", "XU030") 141 142 Returns: 143 DataFrame with all futures and options for the symbol. 144 """ 145 symbol = symbol.upper() 146 147 futures = self._provider.get_futures("all") 148 options = self._provider.get_options("all") 149 150 # Filter out empty DataFrames before concat 151 dfs = [df for df in [futures, options] if not df.empty] 152 if not dfs: 153 return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"]) 154 155 all_data = pd.concat(dfs, ignore_index=True) 156 157 # Filter by symbol in contract name or code 158 mask = ( 159 all_data["contract"].str.upper().str.contains(symbol, na=False) | 160 all_data["code"].str.upper().str.contains(symbol, na=False) 161 ) 162 163 return all_data[mask].reset_index(drop=True)
VİOP (Vadeli İşlem ve Opsiyon Piyasası) data access.
Provides access to Turkish derivatives market data including futures and options contracts.
Data source: İş Yatırım (HTML scraping) Note: Data is delayed by ~15 minutes
Examples:
from borsapy import VIOP viop = VIOP() viop.futures # All futures contracts viop.stock_futures # Stock futures only viop.options # All options contracts
31 def __init__(self) -> None: 32 """Initialize VİOP data accessor.""" 33 self._provider = get_viop_provider()
Initialize VİOP data accessor.
35 @cached_property 36 def futures(self) -> pd.DataFrame: 37 """ 38 Get all futures contracts. 39 40 Returns: 41 DataFrame with columns: 42 - code: Contract code (e.g., F_AKBNK0226) 43 - contract: Contract name (e.g., AKBNK Åžubat 2026 Vadeli) 44 - price: Last price 45 - change: Price change 46 - volume_tl: Trading volume in TL 47 - volume_qty: Trading volume in contracts 48 - category: stock, index, currency, or commodity 49 """ 50 return self._provider.get_futures("all")
Get all futures contracts.
Returns: DataFrame with columns: - code: Contract code (e.g., F_AKBNK0226) - contract: Contract name (e.g., AKBNK Åžubat 2026 Vadeli) - price: Last price - change: Price change - volume_tl: Trading volume in TL - volume_qty: Trading volume in contracts - category: stock, index, currency, or commodity
52 @cached_property 53 def stock_futures(self) -> pd.DataFrame: 54 """ 55 Get stock futures contracts (Pay Vadeli İşlem). 56 57 Returns: 58 DataFrame with futures on individual stocks. 59 """ 60 return self._provider.get_futures("stock")
Get stock futures contracts (Pay Vadeli İşlem).
Returns: DataFrame with futures on individual stocks.
62 @cached_property 63 def index_futures(self) -> pd.DataFrame: 64 """ 65 Get index futures contracts (Endeks Vadeli İşlem). 66 67 Includes XU030, XLBNK, etc. 68 69 Returns: 70 DataFrame with index futures. 71 """ 72 return self._provider.get_futures("index")
Get index futures contracts (Endeks Vadeli İşlem).
Includes XU030, XLBNK, etc.
Returns: DataFrame with index futures.
74 @cached_property 75 def currency_futures(self) -> pd.DataFrame: 76 """ 77 Get currency futures contracts (Döviz Vadeli İşlem). 78 79 Includes USD/TRY, EUR/TRY, etc. 80 81 Returns: 82 DataFrame with currency futures. 83 """ 84 return self._provider.get_futures("currency")
Get currency futures contracts (Döviz Vadeli İşlem).
Includes USD/TRY, EUR/TRY, etc.
Returns: DataFrame with currency futures.
86 @cached_property 87 def commodity_futures(self) -> pd.DataFrame: 88 """ 89 Get commodity futures contracts (Kıymetli Madenler). 90 91 Includes gold, silver, platinum, palladium. 92 93 Returns: 94 DataFrame with commodity futures. 95 """ 96 return self._provider.get_futures("commodity")
Get commodity futures contracts (Kıymetli Madenler).
Includes gold, silver, platinum, palladium.
Returns: DataFrame with commodity futures.
98 @cached_property 99 def options(self) -> pd.DataFrame: 100 """ 101 Get all options contracts. 102 103 Returns: 104 DataFrame with columns: 105 - code: Contract code 106 - contract: Contract name 107 - price: Last price 108 - change: Price change 109 - volume_tl: Trading volume in TL 110 - volume_qty: Trading volume in contracts 111 - category: stock or index 112 """ 113 return self._provider.get_options("all")
Get all options contracts.
Returns: DataFrame with columns: - code: Contract code - contract: Contract name - price: Last price - change: Price change - volume_tl: Trading volume in TL - volume_qty: Trading volume in contracts - category: stock or index
115 @cached_property 116 def stock_options(self) -> pd.DataFrame: 117 """ 118 Get stock options contracts (Pay Opsiyon). 119 120 Returns: 121 DataFrame with options on individual stocks. 122 """ 123 return self._provider.get_options("stock")
Get stock options contracts (Pay Opsiyon).
Returns: DataFrame with options on individual stocks.
125 @cached_property 126 def index_options(self) -> pd.DataFrame: 127 """ 128 Get index options contracts (Endeks Opsiyon). 129 130 Returns: 131 DataFrame with index options. 132 """ 133 return self._provider.get_options("index")
Get index options contracts (Endeks Opsiyon).
Returns: DataFrame with index options.
135 def get_by_symbol(self, symbol: str) -> pd.DataFrame: 136 """ 137 Get all derivatives for a specific underlying symbol. 138 139 Args: 140 symbol: Underlying symbol (e.g., "AKBNK", "THYAO", "XU030") 141 142 Returns: 143 DataFrame with all futures and options for the symbol. 144 """ 145 symbol = symbol.upper() 146 147 futures = self._provider.get_futures("all") 148 options = self._provider.get_options("all") 149 150 # Filter out empty DataFrames before concat 151 dfs = [df for df in [futures, options] if not df.empty] 152 if not dfs: 153 return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"]) 154 155 all_data = pd.concat(dfs, ignore_index=True) 156 157 # Filter by symbol in contract name or code 158 mask = ( 159 all_data["contract"].str.upper().str.contains(symbol, na=False) | 160 all_data["code"].str.upper().str.contains(symbol, na=False) 161 ) 162 163 return all_data[mask].reset_index(drop=True)
Get all derivatives for a specific underlying symbol.
Args: symbol: Underlying symbol (e.g., "AKBNK", "THYAO", "XU030")
Returns: DataFrame with all futures and options for the symbol.
11class Bond: 12 """ 13 A yfinance-like interface for Turkish government bond data. 14 15 Data source: doviz.com/tahvil 16 17 Examples: 18 >>> import borsapy as bp 19 >>> bond = bp.Bond("10Y") 20 >>> bond.yield_rate # Current yield (e.g., 28.03) 21 28.03 22 >>> bond.yield_decimal # As decimal (e.g., 0.2803) 23 0.2803 24 >>> bond.change_pct # Daily change percentage 25 1.5 26 27 >>> bp.bonds() # Get all bond yields 28 name maturity yield change change_pct 29 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54 30 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31 31 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52 32 """ 33 34 # Valid maturities 35 MATURITIES = ["2Y", "5Y", "10Y"] 36 37 def __init__(self, maturity: str): 38 """ 39 Initialize a Bond object. 40 41 Args: 42 maturity: Bond maturity (2Y, 5Y, 10Y). 43 """ 44 self._maturity = maturity.upper() 45 self._provider = get_tahvil_provider() 46 self._data_cache: dict[str, Any] | None = None 47 48 @property 49 def maturity(self) -> str: 50 """Return the bond maturity.""" 51 return self._maturity 52 53 @property 54 def _data(self) -> dict[str, Any]: 55 """Get bond data (cached).""" 56 if self._data_cache is None: 57 self._data_cache = self._provider.get_bond(self._maturity) 58 return self._data_cache 59 60 @property 61 def name(self) -> str: 62 """Return the bond name.""" 63 return self._data.get("name", "") 64 65 @property 66 def yield_rate(self) -> float | None: 67 """ 68 Return the current yield as percentage. 69 70 Returns: 71 Yield rate as percentage (e.g., 28.03 for 28.03%). 72 """ 73 return self._data.get("yield") 74 75 @property 76 def yield_decimal(self) -> float | None: 77 """ 78 Return the current yield as decimal. 79 80 Returns: 81 Yield rate as decimal (e.g., 0.2803 for 28.03%). 82 Useful for financial calculations. 83 """ 84 return self._data.get("yield_decimal") 85 86 @property 87 def change(self) -> float | None: 88 """Return the absolute change in yield.""" 89 return self._data.get("change") 90 91 @property 92 def change_pct(self) -> float | None: 93 """Return the percentage change in yield.""" 94 return self._data.get("change_pct") 95 96 @property 97 def info(self) -> dict[str, Any]: 98 """ 99 Return all bond information. 100 101 Returns: 102 Dictionary with name, maturity, yield, change, etc. 103 """ 104 return self._data.copy() 105 106 def __repr__(self) -> str: 107 return f"Bond('{self._maturity}')"
A yfinance-like interface for Turkish government bond data.
Data source: doviz.com/tahvil
Examples:
import borsapy as bp bond = bp.Bond("10Y") bond.yield_rate # Current yield (e.g., 28.03) 28.03 bond.yield_decimal # As decimal (e.g., 0.2803) 0.2803 bond.change_pct # Daily change percentage 1.5
>>> bp.bonds() # Get all bond yields name maturity yield change change_pct 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
37 def __init__(self, maturity: str): 38 """ 39 Initialize a Bond object. 40 41 Args: 42 maturity: Bond maturity (2Y, 5Y, 10Y). 43 """ 44 self._maturity = maturity.upper() 45 self._provider = get_tahvil_provider() 46 self._data_cache: dict[str, Any] | None = None
Initialize a Bond object.
Args: maturity: Bond maturity (2Y, 5Y, 10Y).
48 @property 49 def maturity(self) -> str: 50 """Return the bond maturity.""" 51 return self._maturity
Return the bond maturity.
60 @property 61 def name(self) -> str: 62 """Return the bond name.""" 63 return self._data.get("name", "")
Return the bond name.
65 @property 66 def yield_rate(self) -> float | None: 67 """ 68 Return the current yield as percentage. 69 70 Returns: 71 Yield rate as percentage (e.g., 28.03 for 28.03%). 72 """ 73 return self._data.get("yield")
Return the current yield as percentage.
Returns: Yield rate as percentage (e.g., 28.03 for 28.03%).
75 @property 76 def yield_decimal(self) -> float | None: 77 """ 78 Return the current yield as decimal. 79 80 Returns: 81 Yield rate as decimal (e.g., 0.2803 for 28.03%). 82 Useful for financial calculations. 83 """ 84 return self._data.get("yield_decimal")
Return the current yield as decimal.
Returns: Yield rate as decimal (e.g., 0.2803 for 28.03%). Useful for financial calculations.
86 @property 87 def change(self) -> float | None: 88 """Return the absolute change in yield.""" 89 return self._data.get("change")
Return the absolute change in yield.
91 @property 92 def change_pct(self) -> float | None: 93 """Return the percentage change in yield.""" 94 return self._data.get("change_pct")
Return the percentage change in yield.
96 @property 97 def info(self) -> dict[str, Any]: 98 """ 99 Return all bond information. 100 101 Returns: 102 Dictionary with name, maturity, yield, change, etc. 103 """ 104 return self._data.copy()
Return all bond information.
Returns: Dictionary with name, maturity, yield, change, etc.
33class Eurobond: 34 """Single Turkish sovereign Eurobond interface. 35 36 Provides access to bond data including prices, yields, 37 maturity, and other characteristics. 38 39 Attributes: 40 isin: ISIN code of the bond. 41 maturity: Maturity date. 42 days_to_maturity: Days until maturity. 43 currency: Bond currency (USD or EUR). 44 bid_price: Bid price (buying price). 45 bid_yield: Bid yield (buying yield). 46 ask_price: Ask price (selling price). 47 ask_yield: Ask yield (selling yield). 48 info: All bond data as dictionary. 49 50 Examples: 51 >>> bond = Eurobond("US900123DG28") 52 >>> bond.bid_yield 53 6.55 54 >>> bond.currency 55 'USD' 56 """ 57 58 def __init__(self, isin: str): 59 """Initialize Eurobond by ISIN. 60 61 Args: 62 isin: ISIN code (e.g., "US900123DG28"). 63 64 Raises: 65 DataNotAvailableError: If bond not found. 66 """ 67 self._isin = isin.upper() 68 self._provider = get_eurobond_provider() 69 self._data_cache: dict | None = None 70 71 @property 72 def _data(self) -> dict: 73 """Lazy-loaded bond data.""" 74 if self._data_cache is None: 75 self._data_cache = self._provider.get_eurobond(self._isin) 76 if self._data_cache is None: 77 raise DataNotAvailableError(f"Eurobond not found: {self._isin}") 78 return self._data_cache 79 80 @property 81 def isin(self) -> str: 82 """ISIN code of the bond.""" 83 return self._data["isin"] 84 85 @property 86 def maturity(self) -> datetime | None: 87 """Maturity date of the bond.""" 88 return self._data.get("maturity") 89 90 @property 91 def days_to_maturity(self) -> int: 92 """Number of days until maturity.""" 93 return self._data.get("days_to_maturity", 0) 94 95 @property 96 def currency(self) -> str: 97 """Bond currency (USD or EUR).""" 98 return self._data.get("currency", "") 99 100 @property 101 def bid_price(self) -> float | None: 102 """Bid price (buying price).""" 103 return self._data.get("bid_price") 104 105 @property 106 def bid_yield(self) -> float | None: 107 """Bid yield (buying yield) as percentage.""" 108 return self._data.get("bid_yield") 109 110 @property 111 def ask_price(self) -> float | None: 112 """Ask price (selling price).""" 113 return self._data.get("ask_price") 114 115 @property 116 def ask_yield(self) -> float | None: 117 """Ask yield (selling yield) as percentage.""" 118 return self._data.get("ask_yield") 119 120 @property 121 def info(self) -> dict: 122 """All bond data as dictionary. 123 124 Returns: 125 Dict with all bond attributes. 126 """ 127 return self._data.copy() 128 129 def __repr__(self) -> str: 130 """String representation.""" 131 try: 132 maturity_year = self.maturity.year if self.maturity else "?" 133 return f"Eurobond({self._isin}, {self.currency}, {maturity_year}, yield={self.bid_yield}%)" 134 except DataNotAvailableError: 135 return f"Eurobond({self._isin})"
Single Turkish sovereign Eurobond interface.
Provides access to bond data including prices, yields, maturity, and other characteristics.
Attributes: isin: ISIN code of the bond. maturity: Maturity date. days_to_maturity: Days until maturity. currency: Bond currency (USD or EUR). bid_price: Bid price (buying price). bid_yield: Bid yield (buying yield). ask_price: Ask price (selling price). ask_yield: Ask yield (selling yield). info: All bond data as dictionary.
Examples:
bond = Eurobond("US900123DG28") bond.bid_yield 6.55 bond.currency 'USD'
58 def __init__(self, isin: str): 59 """Initialize Eurobond by ISIN. 60 61 Args: 62 isin: ISIN code (e.g., "US900123DG28"). 63 64 Raises: 65 DataNotAvailableError: If bond not found. 66 """ 67 self._isin = isin.upper() 68 self._provider = get_eurobond_provider() 69 self._data_cache: dict | None = None
Initialize Eurobond by ISIN.
Args: isin: ISIN code (e.g., "US900123DG28").
Raises: DataNotAvailableError: If bond not found.
85 @property 86 def maturity(self) -> datetime | None: 87 """Maturity date of the bond.""" 88 return self._data.get("maturity")
Maturity date of the bond.
90 @property 91 def days_to_maturity(self) -> int: 92 """Number of days until maturity.""" 93 return self._data.get("days_to_maturity", 0)
Number of days until maturity.
95 @property 96 def currency(self) -> str: 97 """Bond currency (USD or EUR).""" 98 return self._data.get("currency", "")
Bond currency (USD or EUR).
100 @property 101 def bid_price(self) -> float | None: 102 """Bid price (buying price).""" 103 return self._data.get("bid_price")
Bid price (buying price).
105 @property 106 def bid_yield(self) -> float | None: 107 """Bid yield (buying yield) as percentage.""" 108 return self._data.get("bid_yield")
Bid yield (buying yield) as percentage.
110 @property 111 def ask_price(self) -> float | None: 112 """Ask price (selling price).""" 113 return self._data.get("ask_price")
Ask price (selling price).
34class TCMB: 35 """TCMB interest rates interface. 36 37 Provides access to Turkish Central Bank policy rates including 38 the 1-week repo rate (main policy rate), overnight corridor, 39 and late liquidity window rates. 40 41 Attributes: 42 policy_rate: Current 1-week repo rate (policy rate). 43 overnight: Overnight corridor rates (borrowing/lending). 44 late_liquidity: Late liquidity window rates (borrowing/lending). 45 rates: DataFrame with all current rates. 46 47 Examples: 48 >>> tcmb = TCMB() 49 >>> tcmb.policy_rate 50 38.0 51 >>> tcmb.overnight 52 {'borrowing': 36.5, 'lending': 41.0} 53 """ 54 55 def __init__(self): 56 """Initialize TCMB interface.""" 57 self._provider = get_tcmb_rates_provider() 58 59 @property 60 def policy_rate(self) -> float | None: 61 """Get current 1-week repo rate (policy rate). 62 63 This is the main policy interest rate set by TCMB. 64 65 Returns: 66 Policy rate as percentage (e.g., 38.0 for 38%). 67 """ 68 data = self._provider.get_policy_rate() 69 return data.get("lending") 70 71 @property 72 def overnight(self) -> dict: 73 """Get overnight (O/N) corridor rates. 74 75 The overnight corridor defines the band within which 76 short-term interest rates can fluctuate. 77 78 Returns: 79 Dict with 'borrowing' and 'lending' rates. 80 81 Example: 82 {'borrowing': 36.5, 'lending': 41.0} 83 """ 84 data = self._provider.get_overnight_rates() 85 return { 86 "borrowing": data.get("borrowing"), 87 "lending": data.get("lending"), 88 } 89 90 @property 91 def late_liquidity(self) -> dict: 92 """Get late liquidity window (LON) rates. 93 94 The late liquidity window is a facility for banks 95 to borrow/lend at the end of the day. 96 97 Returns: 98 Dict with 'borrowing' and 'lending' rates. 99 100 Example: 101 {'borrowing': 0.0, 'lending': 44.0} 102 """ 103 data = self._provider.get_late_liquidity_rates() 104 return { 105 "borrowing": data.get("borrowing"), 106 "lending": data.get("lending"), 107 } 108 109 @property 110 def rates(self) -> pd.DataFrame: 111 """Get all current rates as DataFrame. 112 113 Returns: 114 DataFrame with columns: type, borrowing, lending. 115 116 Example: 117 >>> tcmb.rates 118 type borrowing lending 119 0 policy None 38.0 120 1 overnight 36.5 41.0 121 2 late_liquidity 0.0 44.0 122 """ 123 data = self._provider.get_all_rates() 124 df = pd.DataFrame(data) 125 if "rate_type" in df.columns: 126 df = df.rename(columns={"rate_type": "type"}) 127 return df[["type", "borrowing", "lending"]] 128 129 def history( 130 self, 131 rate_type: str = "policy", 132 period: str | None = None, 133 ) -> pd.DataFrame: 134 """Get historical rates for given type. 135 136 Args: 137 rate_type: One of "policy", "overnight", "late_liquidity". 138 period: Optional period filter (e.g., "1y", "5y", "max"). 139 If None, returns all available data. 140 141 Returns: 142 DataFrame with date index and borrowing/lending columns. 143 144 Example: 145 >>> tcmb.history("policy", period="1y") 146 borrowing lending 147 date 148 2024-01-25 None 45.0 149 2024-02-22 None 45.0 150 ... 151 """ 152 data = self._provider.get_rate_history(rate_type) 153 154 if not data: 155 return pd.DataFrame(columns=["date", "borrowing", "lending"]) 156 157 df = pd.DataFrame(data) 158 df["date"] = pd.to_datetime(df["date"]) 159 df = df.set_index("date").sort_index() 160 161 # Apply period filter if specified 162 if period: 163 end_date = datetime.now() 164 period_map = { 165 "1w": timedelta(days=7), 166 "1mo": timedelta(days=30), 167 "3mo": timedelta(days=90), 168 "6mo": timedelta(days=180), 169 "1y": timedelta(days=365), 170 "2y": timedelta(days=730), 171 "5y": timedelta(days=1825), 172 "10y": timedelta(days=3650), 173 } 174 175 if period.lower() in period_map: 176 start_date = end_date - period_map[period.lower()] 177 df = df[df.index >= start_date] 178 # "max" or unknown period returns all data 179 180 return df 181 182 def __repr__(self) -> str: 183 """String representation.""" 184 rate = self.policy_rate 185 if rate is not None: 186 return f"TCMB(policy_rate={rate}%)" 187 return "TCMB()"
TCMB interest rates interface.
Provides access to Turkish Central Bank policy rates including the 1-week repo rate (main policy rate), overnight corridor, and late liquidity window rates.
Attributes: policy_rate: Current 1-week repo rate (policy rate). overnight: Overnight corridor rates (borrowing/lending). late_liquidity: Late liquidity window rates (borrowing/lending). rates: DataFrame with all current rates.
Examples:
tcmb = TCMB() tcmb.policy_rate 38.0 tcmb.overnight {'borrowing': 36.5, 'lending': 41.0}
55 def __init__(self): 56 """Initialize TCMB interface.""" 57 self._provider = get_tcmb_rates_provider()
Initialize TCMB interface.
59 @property 60 def policy_rate(self) -> float | None: 61 """Get current 1-week repo rate (policy rate). 62 63 This is the main policy interest rate set by TCMB. 64 65 Returns: 66 Policy rate as percentage (e.g., 38.0 for 38%). 67 """ 68 data = self._provider.get_policy_rate() 69 return data.get("lending")
Get current 1-week repo rate (policy rate).
This is the main policy interest rate set by TCMB.
Returns: Policy rate as percentage (e.g., 38.0 for 38%).
71 @property 72 def overnight(self) -> dict: 73 """Get overnight (O/N) corridor rates. 74 75 The overnight corridor defines the band within which 76 short-term interest rates can fluctuate. 77 78 Returns: 79 Dict with 'borrowing' and 'lending' rates. 80 81 Example: 82 {'borrowing': 36.5, 'lending': 41.0} 83 """ 84 data = self._provider.get_overnight_rates() 85 return { 86 "borrowing": data.get("borrowing"), 87 "lending": data.get("lending"), 88 }
Get overnight (O/N) corridor rates.
The overnight corridor defines the band within which short-term interest rates can fluctuate.
Returns: Dict with 'borrowing' and 'lending' rates.
Example: {'borrowing': 36.5, 'lending': 41.0}
90 @property 91 def late_liquidity(self) -> dict: 92 """Get late liquidity window (LON) rates. 93 94 The late liquidity window is a facility for banks 95 to borrow/lend at the end of the day. 96 97 Returns: 98 Dict with 'borrowing' and 'lending' rates. 99 100 Example: 101 {'borrowing': 0.0, 'lending': 44.0} 102 """ 103 data = self._provider.get_late_liquidity_rates() 104 return { 105 "borrowing": data.get("borrowing"), 106 "lending": data.get("lending"), 107 }
Get late liquidity window (LON) rates.
The late liquidity window is a facility for banks to borrow/lend at the end of the day.
Returns: Dict with 'borrowing' and 'lending' rates.
Example: {'borrowing': 0.0, 'lending': 44.0}
109 @property 110 def rates(self) -> pd.DataFrame: 111 """Get all current rates as DataFrame. 112 113 Returns: 114 DataFrame with columns: type, borrowing, lending. 115 116 Example: 117 >>> tcmb.rates 118 type borrowing lending 119 0 policy None 38.0 120 1 overnight 36.5 41.0 121 2 late_liquidity 0.0 44.0 122 """ 123 data = self._provider.get_all_rates() 124 df = pd.DataFrame(data) 125 if "rate_type" in df.columns: 126 df = df.rename(columns={"rate_type": "type"}) 127 return df[["type", "borrowing", "lending"]]
Get all current rates as DataFrame.
Returns: DataFrame with columns: type, borrowing, lending.
Example:
tcmb.rates type borrowing lending 0 policy None 38.0 1 overnight 36.5 41.0 2 late_liquidity 0.0 44.0
129 def history( 130 self, 131 rate_type: str = "policy", 132 period: str | None = None, 133 ) -> pd.DataFrame: 134 """Get historical rates for given type. 135 136 Args: 137 rate_type: One of "policy", "overnight", "late_liquidity". 138 period: Optional period filter (e.g., "1y", "5y", "max"). 139 If None, returns all available data. 140 141 Returns: 142 DataFrame with date index and borrowing/lending columns. 143 144 Example: 145 >>> tcmb.history("policy", period="1y") 146 borrowing lending 147 date 148 2024-01-25 None 45.0 149 2024-02-22 None 45.0 150 ... 151 """ 152 data = self._provider.get_rate_history(rate_type) 153 154 if not data: 155 return pd.DataFrame(columns=["date", "borrowing", "lending"]) 156 157 df = pd.DataFrame(data) 158 df["date"] = pd.to_datetime(df["date"]) 159 df = df.set_index("date").sort_index() 160 161 # Apply period filter if specified 162 if period: 163 end_date = datetime.now() 164 period_map = { 165 "1w": timedelta(days=7), 166 "1mo": timedelta(days=30), 167 "3mo": timedelta(days=90), 168 "6mo": timedelta(days=180), 169 "1y": timedelta(days=365), 170 "2y": timedelta(days=730), 171 "5y": timedelta(days=1825), 172 "10y": timedelta(days=3650), 173 } 174 175 if period.lower() in period_map: 176 start_date = end_date - period_map[period.lower()] 177 df = df[df.index >= start_date] 178 # "max" or unknown period returns all data 179 180 return df
Get historical rates for given type.
Args: rate_type: One of "policy", "overnight", "late_liquidity". period: Optional period filter (e.g., "1y", "5y", "max"). If None, returns all available data.
Returns: DataFrame with date index and borrowing/lending columns.
Example:
tcmb.history("policy", period="1y") borrowing lending date 2024-01-25 None 45.0 2024-02-22 None 45.0 ...
11class EconomicCalendar: 12 """ 13 A yfinance-like interface for economic calendar data. 14 15 Data source: doviz.com 16 17 Examples: 18 >>> import borsapy as bp 19 >>> cal = bp.EconomicCalendar() 20 >>> cal.events(period="1w") # This week's events 21 Date Time Country Importance Event Actual Forecast Previous 22 0 2024-01-15 10:00:00 Türkiye high Enflasyon (YoY) 64.77% 65.00% 61.98% 23 ... 24 25 >>> cal.today() # Today's events 26 >>> cal.events(country="TR", importance="high") # High importance TR events 27 """ 28 29 # Valid country codes 30 COUNTRIES = ["TR", "US", "EU", "DE", "GB", "JP", "CN", "FR", "IT", "CA", "AU", "CH"] 31 32 def __init__(self): 33 """Initialize EconomicCalendar.""" 34 self._provider = get_calendar_provider() 35 36 def events( 37 self, 38 period: str = "1w", 39 start: datetime | str | None = None, 40 end: datetime | str | None = None, 41 country: str | list[str] | None = None, 42 importance: str | None = None, 43 ) -> pd.DataFrame: 44 """ 45 Get economic calendar events. 46 47 Args: 48 period: How much data to fetch. Valid periods: 1d, 1w, 2w, 1mo. 49 Ignored if start is provided. 50 start: Start date (string or datetime). 51 end: End date (string or datetime). Defaults to start + period. 52 country: Country code(s) to filter by (TR, US, EU, etc.). 53 Can be a single code or list of codes. 54 Defaults to ['TR', 'US']. 55 importance: Filter by importance level ('low', 'mid', 'high'). 56 57 Returns: 58 DataFrame with columns: Date, Time, Country, Importance, Event, 59 Actual, Forecast, Previous, Period. 60 61 Examples: 62 >>> cal = EconomicCalendar() 63 >>> cal.events(period="1w") # Next 7 days 64 >>> cal.events(country="TR", importance="high") # High importance TR events 65 >>> cal.events(country=["TR", "US", "EU"]) # Multiple countries 66 >>> cal.events(start="2024-01-01", end="2024-01-31") # Date range 67 """ 68 # Parse dates 69 start_dt = self._parse_date(start) if start else None 70 end_dt = self._parse_date(end) if end else None 71 72 # If no start, use today 73 if start_dt is None: 74 start_dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 75 76 # If no end, calculate from period 77 if end_dt is None: 78 days = {"1d": 1, "1w": 7, "2w": 14, "1mo": 30}.get(period, 7) 79 end_dt = start_dt + timedelta(days=days) 80 81 # Parse country parameter 82 countries = self._parse_countries(country) 83 84 # Fetch events 85 events = self._provider.get_economic_calendar( 86 start=start_dt, 87 end=end_dt, 88 countries=countries, 89 importance=importance, 90 ) 91 92 # Convert to DataFrame 93 if not events: 94 return pd.DataFrame( 95 columns=[ 96 "Date", 97 "Time", 98 "Country", 99 "Importance", 100 "Event", 101 "Actual", 102 "Forecast", 103 "Previous", 104 "Period", 105 ] 106 ) 107 108 df = pd.DataFrame(events) 109 110 # Rename columns 111 df = df.rename( 112 columns={ 113 "date": "Date", 114 "time": "Time", 115 "country": "Country", 116 "importance": "Importance", 117 "event": "Event", 118 "actual": "Actual", 119 "forecast": "Forecast", 120 "previous": "Previous", 121 "period": "Period", 122 } 123 ) 124 125 # Drop internal columns 126 if "country_code" in df.columns: 127 df = df.drop(columns=["country_code"]) 128 129 # Reorder columns 130 column_order = [ 131 "Date", 132 "Time", 133 "Country", 134 "Importance", 135 "Event", 136 "Actual", 137 "Forecast", 138 "Previous", 139 "Period", 140 ] 141 df = df[[c for c in column_order if c in df.columns]] 142 143 return df 144 145 def today( 146 self, 147 country: str | list[str] | None = None, 148 importance: str | None = None, 149 ) -> pd.DataFrame: 150 """ 151 Get today's economic events. 152 153 Args: 154 country: Country code(s) to filter by. 155 importance: Filter by importance level. 156 157 Returns: 158 DataFrame with today's economic events. 159 """ 160 return self.events(period="1d", country=country, importance=importance) 161 162 def this_week( 163 self, 164 country: str | list[str] | None = None, 165 importance: str | None = None, 166 ) -> pd.DataFrame: 167 """ 168 Get this week's economic events. 169 170 Args: 171 country: Country code(s) to filter by. 172 importance: Filter by importance level. 173 174 Returns: 175 DataFrame with this week's economic events. 176 """ 177 return self.events(period="1w", country=country, importance=importance) 178 179 def this_month( 180 self, 181 country: str | list[str] | None = None, 182 importance: str | None = None, 183 ) -> pd.DataFrame: 184 """ 185 Get this month's economic events. 186 187 Args: 188 country: Country code(s) to filter by. 189 importance: Filter by importance level. 190 191 Returns: 192 DataFrame with this month's economic events. 193 """ 194 return self.events(period="1mo", country=country, importance=importance) 195 196 def high_importance( 197 self, 198 period: str = "1w", 199 country: str | list[str] | None = None, 200 ) -> pd.DataFrame: 201 """ 202 Get high importance events only. 203 204 Args: 205 period: Time period (1d, 1w, 2w, 1mo). 206 country: Country code(s) to filter by. 207 208 Returns: 209 DataFrame with high importance events. 210 """ 211 return self.events(period=period, country=country, importance="high") 212 213 @staticmethod 214 def countries() -> list[str]: 215 """ 216 Get list of supported country codes. 217 218 Returns: 219 List of country codes. 220 221 Examples: 222 >>> EconomicCalendar.countries() 223 ['TR', 'US', 'EU', 'DE', 'GB', 'JP', 'CN', ...] 224 """ 225 return EconomicCalendar.COUNTRIES.copy() 226 227 def _parse_date(self, date: str | datetime) -> datetime: 228 """Parse a date string to datetime.""" 229 if isinstance(date, datetime): 230 return date 231 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 232 try: 233 return datetime.strptime(date, fmt) 234 except ValueError: 235 continue 236 raise ValueError(f"Could not parse date: {date}") 237 238 def _parse_countries(self, country: str | list[str] | None) -> list[str]: 239 """Parse country parameter to list of codes.""" 240 if country is None: 241 return ["TR", "US"] 242 if isinstance(country, str): 243 return [country.upper()] 244 return [c.upper() for c in country] 245 246 def __repr__(self) -> str: 247 return "EconomicCalendar()"
A yfinance-like interface for economic calendar data.
Data source: doviz.com
Examples:
import borsapy as bp cal = bp.EconomicCalendar() cal.events(period="1w") # This week's events Date Time Country Importance Event Actual Forecast Previous 0 2024-01-15 10:00:00 Türkiye high Enflasyon (YoY) 64.77% 65.00% 61.98% ...
>>> cal.today() # Today's events >>> cal.events(country="TR", importance="high") # High importance TR events
32 def __init__(self): 33 """Initialize EconomicCalendar.""" 34 self._provider = get_calendar_provider()
Initialize EconomicCalendar.
36 def events( 37 self, 38 period: str = "1w", 39 start: datetime | str | None = None, 40 end: datetime | str | None = None, 41 country: str | list[str] | None = None, 42 importance: str | None = None, 43 ) -> pd.DataFrame: 44 """ 45 Get economic calendar events. 46 47 Args: 48 period: How much data to fetch. Valid periods: 1d, 1w, 2w, 1mo. 49 Ignored if start is provided. 50 start: Start date (string or datetime). 51 end: End date (string or datetime). Defaults to start + period. 52 country: Country code(s) to filter by (TR, US, EU, etc.). 53 Can be a single code or list of codes. 54 Defaults to ['TR', 'US']. 55 importance: Filter by importance level ('low', 'mid', 'high'). 56 57 Returns: 58 DataFrame with columns: Date, Time, Country, Importance, Event, 59 Actual, Forecast, Previous, Period. 60 61 Examples: 62 >>> cal = EconomicCalendar() 63 >>> cal.events(period="1w") # Next 7 days 64 >>> cal.events(country="TR", importance="high") # High importance TR events 65 >>> cal.events(country=["TR", "US", "EU"]) # Multiple countries 66 >>> cal.events(start="2024-01-01", end="2024-01-31") # Date range 67 """ 68 # Parse dates 69 start_dt = self._parse_date(start) if start else None 70 end_dt = self._parse_date(end) if end else None 71 72 # If no start, use today 73 if start_dt is None: 74 start_dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) 75 76 # If no end, calculate from period 77 if end_dt is None: 78 days = {"1d": 1, "1w": 7, "2w": 14, "1mo": 30}.get(period, 7) 79 end_dt = start_dt + timedelta(days=days) 80 81 # Parse country parameter 82 countries = self._parse_countries(country) 83 84 # Fetch events 85 events = self._provider.get_economic_calendar( 86 start=start_dt, 87 end=end_dt, 88 countries=countries, 89 importance=importance, 90 ) 91 92 # Convert to DataFrame 93 if not events: 94 return pd.DataFrame( 95 columns=[ 96 "Date", 97 "Time", 98 "Country", 99 "Importance", 100 "Event", 101 "Actual", 102 "Forecast", 103 "Previous", 104 "Period", 105 ] 106 ) 107 108 df = pd.DataFrame(events) 109 110 # Rename columns 111 df = df.rename( 112 columns={ 113 "date": "Date", 114 "time": "Time", 115 "country": "Country", 116 "importance": "Importance", 117 "event": "Event", 118 "actual": "Actual", 119 "forecast": "Forecast", 120 "previous": "Previous", 121 "period": "Period", 122 } 123 ) 124 125 # Drop internal columns 126 if "country_code" in df.columns: 127 df = df.drop(columns=["country_code"]) 128 129 # Reorder columns 130 column_order = [ 131 "Date", 132 "Time", 133 "Country", 134 "Importance", 135 "Event", 136 "Actual", 137 "Forecast", 138 "Previous", 139 "Period", 140 ] 141 df = df[[c for c in column_order if c in df.columns]] 142 143 return df
Get economic calendar events.
Args: period: How much data to fetch. Valid periods: 1d, 1w, 2w, 1mo. Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to start + period. country: Country code(s) to filter by (TR, US, EU, etc.). Can be a single code or list of codes. Defaults to ['TR', 'US']. importance: Filter by importance level ('low', 'mid', 'high').
Returns: DataFrame with columns: Date, Time, Country, Importance, Event, Actual, Forecast, Previous, Period.
Examples:
cal = EconomicCalendar() cal.events(period="1w") # Next 7 days cal.events(country="TR", importance="high") # High importance TR events cal.events(country=["TR", "US", "EU"]) # Multiple countries cal.events(start="2024-01-01", end="2024-01-31") # Date range
145 def today( 146 self, 147 country: str | list[str] | None = None, 148 importance: str | None = None, 149 ) -> pd.DataFrame: 150 """ 151 Get today's economic events. 152 153 Args: 154 country: Country code(s) to filter by. 155 importance: Filter by importance level. 156 157 Returns: 158 DataFrame with today's economic events. 159 """ 160 return self.events(period="1d", country=country, importance=importance)
Get today's economic events.
Args: country: Country code(s) to filter by. importance: Filter by importance level.
Returns: DataFrame with today's economic events.
162 def this_week( 163 self, 164 country: str | list[str] | None = None, 165 importance: str | None = None, 166 ) -> pd.DataFrame: 167 """ 168 Get this week's economic events. 169 170 Args: 171 country: Country code(s) to filter by. 172 importance: Filter by importance level. 173 174 Returns: 175 DataFrame with this week's economic events. 176 """ 177 return self.events(period="1w", country=country, importance=importance)
Get this week's economic events.
Args: country: Country code(s) to filter by. importance: Filter by importance level.
Returns: DataFrame with this week's economic events.
179 def this_month( 180 self, 181 country: str | list[str] | None = None, 182 importance: str | None = None, 183 ) -> pd.DataFrame: 184 """ 185 Get this month's economic events. 186 187 Args: 188 country: Country code(s) to filter by. 189 importance: Filter by importance level. 190 191 Returns: 192 DataFrame with this month's economic events. 193 """ 194 return self.events(period="1mo", country=country, importance=importance)
Get this month's economic events.
Args: country: Country code(s) to filter by. importance: Filter by importance level.
Returns: DataFrame with this month's economic events.
196 def high_importance( 197 self, 198 period: str = "1w", 199 country: str | list[str] | None = None, 200 ) -> pd.DataFrame: 201 """ 202 Get high importance events only. 203 204 Args: 205 period: Time period (1d, 1w, 2w, 1mo). 206 country: Country code(s) to filter by. 207 208 Returns: 209 DataFrame with high importance events. 210 """ 211 return self.events(period=period, country=country, importance="high")
Get high importance events only.
Args: period: Time period (1d, 1w, 2w, 1mo). country: Country code(s) to filter by.
Returns: DataFrame with high importance events.
213 @staticmethod 214 def countries() -> list[str]: 215 """ 216 Get list of supported country codes. 217 218 Returns: 219 List of country codes. 220 221 Examples: 222 >>> EconomicCalendar.countries() 223 ['TR', 'US', 'EU', 'DE', 'GB', 'JP', 'CN', ...] 224 """ 225 return EconomicCalendar.COUNTRIES.copy()
Get list of supported country codes.
Returns: List of country codes.
Examples:
EconomicCalendar.countries() ['TR', 'US', 'EU', 'DE', 'GB', 'JP', 'CN', ...]
12class Screener: 13 """ 14 A yfinance-like interface for BIST stock screening. 15 16 Data source: İş Yatırım 17 18 Examples: 19 >>> import borsapy as bp 20 >>> screener = bp.Screener() 21 >>> screener.add_filter("market_cap", min=1000) # Min $1B market cap 22 >>> screener.add_filter("dividend_yield", min=3) # Min 3% dividend yield 23 >>> results = screener.run() 24 symbol name market_cap dividend_yield 25 0 THYAO Türk Hava Yolları 5234.5 4.2 26 1 GARAN Garanti Bankası 8123.4 5.1 27 ... 28 29 >>> # Using templates 30 >>> results = bp.screen_stocks(template="high_dividend") 31 32 >>> # Direct filtering 33 >>> results = bp.screen_stocks(market_cap_min=1000, pe_max=15) 34 """ 35 36 # Available templates 37 TEMPLATES = [ 38 "small_cap", 39 "mid_cap", 40 "large_cap", 41 "high_dividend", 42 "high_upside", 43 "low_upside", 44 "high_volume", 45 "low_volume", 46 "buy_recommendation", 47 "sell_recommendation", 48 "high_net_margin", 49 "high_return", 50 "low_pe", 51 "high_roe", 52 "high_foreign_ownership", 53 ] 54 55 def __init__(self): 56 """Initialize Screener.""" 57 self._provider = get_screener_provider() 58 self._filters: list[tuple[str, str, str, str]] = [] 59 self._sector: str | None = None 60 self._index: str | None = None 61 self._recommendation: str | None = None 62 63 # Default min/max values for criteria when only one bound is specified 64 # API requires both min and max - these are sensible defaults 65 CRITERIA_DEFAULTS = { 66 "price": {"min": 0, "max": 100000}, 67 "market_cap": {"min": 0, "max": 5000000}, # TL millions 68 "market_cap_usd": {"min": 0, "max": 100000}, # USD millions 69 "pe": {"min": -1000, "max": 10000}, 70 "pb": {"min": -100, "max": 1000}, 71 "ev_ebitda": {"min": -100, "max": 1000}, 72 "ev_sales": {"min": -100, "max": 1000}, 73 "dividend_yield": {"min": 0, "max": 100}, 74 "dividend_yield_2025": {"min": 0, "max": 100}, 75 "roe": {"min": -200, "max": 500}, 76 "roa": {"min": -200, "max": 500}, 77 "net_margin": {"min": -200, "max": 500}, 78 "ebitda_margin": {"min": -200, "max": 500}, 79 "upside_potential": {"min": -100, "max": 500}, 80 "foreign_ratio": {"min": 0, "max": 100}, 81 "float_ratio": {"min": 0, "max": 100}, 82 "return_1w": {"min": -100, "max": 100}, 83 "return_1m": {"min": -100, "max": 200}, 84 "return_1y": {"min": -100, "max": 1000}, 85 "return_ytd": {"min": -100, "max": 1000}, 86 "volume_3m": {"min": 0, "max": 1000}, 87 "volume_12m": {"min": 0, "max": 1000}, # 12 aylık ortalama hacim (mn $) 88 "float_market_cap": {"min": 0, "max": 100000}, # Halka açık piyasa değeri (mn $) 89 } 90 91 def add_filter( 92 self, 93 criteria: str, 94 min: float | None = None, 95 max: float | None = None, 96 required: bool = False, 97 ) -> "Screener": 98 """ 99 Add a filter criterion. 100 101 Args: 102 criteria: Criteria name (market_cap, pe, dividend_yield, etc.). 103 min: Minimum value. 104 max: Maximum value. 105 required: Whether this filter is required. 106 107 Returns: 108 Self for method chaining. 109 110 Examples: 111 >>> screener = Screener() 112 >>> screener.add_filter("market_cap", min=1000) 113 >>> screener.add_filter("pe", max=15) 114 """ 115 # Map criteria name to ID 116 criteria_map = self._provider.CRITERIA_MAP 117 criteria_id = criteria_map.get(criteria.lower(), criteria) 118 119 # Get default bounds for this criteria 120 defaults = self.CRITERIA_DEFAULTS.get(criteria.lower(), {"min": -999999, "max": 999999}) 121 122 # API requires both min and max - use defaults when only one is provided 123 if min is None and max is not None: 124 min = defaults["min"] 125 elif max is None and min is not None: 126 max = defaults["max"] 127 128 min_str = str(min) if min is not None else "" 129 max_str = str(max) if max is not None else "" 130 required_str = "True" if required else "False" 131 132 self._filters.append((criteria_id, min_str, max_str, required_str)) 133 return self 134 135 def set_sector(self, sector: str) -> "Screener": 136 """ 137 Set sector filter. 138 139 Args: 140 sector: Sector name (e.g., "Bankacılık") or ID (e.g., "0001"). 141 142 Returns: 143 Self for method chaining. 144 """ 145 # Convert sector name to ID if needed 146 if sector and not sector.startswith("0"): 147 sectors_data = self._provider.get_sectors() 148 for s in sectors_data: 149 if s.get("name", "").lower() == sector.lower(): 150 sector = s.get("id", sector) 151 break 152 self._sector = sector 153 return self 154 155 def set_index(self, index: str) -> "Screener": 156 """ 157 Set index filter. 158 159 Args: 160 index: Index name (e.g., "BIST 30", "BIST 100"). 161 162 Returns: 163 Self for method chaining. 164 """ 165 # Note: Index filtering may have limited support in the API 166 self._index = index 167 return self 168 169 def set_recommendation(self, recommendation: str) -> "Screener": 170 """ 171 Set recommendation filter. 172 173 Args: 174 recommendation: Recommendation type ("AL", "SAT", "TUT"). 175 176 Returns: 177 Self for method chaining. 178 """ 179 self._recommendation = recommendation.upper() 180 return self 181 182 def clear(self) -> "Screener": 183 """ 184 Clear all filters. 185 186 Returns: 187 Self for method chaining. 188 """ 189 self._filters = [] 190 self._sector = None 191 self._index = None 192 self._recommendation = None 193 return self 194 195 def run(self, template: str | None = None) -> pd.DataFrame: 196 """ 197 Run the screener and return results. 198 199 Args: 200 template: Optional pre-defined template to use. 201 202 Returns: 203 DataFrame with matching stocks. 204 """ 205 # Note: İş Yatırım API doesn't support index filtering directly, 206 # so we filter locally using BIST index components 207 results = self._provider.screen( 208 criterias=self._filters if self._filters else None, 209 sector=self._sector, 210 index=None, # API doesn't support this, we filter locally 211 recommendation=self._recommendation, 212 template=template, 213 ) 214 215 if not results: 216 return pd.DataFrame(columns=["symbol", "name"]) 217 218 df = pd.DataFrame(results) 219 220 # Filter by index if specified 221 if self._index and not df.empty: 222 df = self._filter_by_index(df, self._index) 223 224 return df 225 226 def _filter_by_index(self, df: pd.DataFrame, index: str) -> pd.DataFrame: 227 """Filter DataFrame to only include symbols in the specified index.""" 228 # Normalize index name to code (e.g., "BIST 30" -> "XU030") 229 index_map = { 230 "BIST 30": "XU030", 231 "BIST30": "XU030", 232 "BIST 50": "XU050", 233 "BIST50": "XU050", 234 "BIST 100": "XU100", 235 "BIST100": "XU100", 236 "BIST BANKA": "XBANK", 237 "BIST SINAİ": "XUSIN", 238 "BIST HİZMETLER": "XUHIZ", 239 "BIST TEKNOLOJİ": "XUTEK", 240 } 241 index_code = index_map.get(index.upper().replace("_", " ").replace("-", " "), index.upper()) 242 243 try: 244 provider = get_bist_index_provider() 245 components = provider.get_components(index_code) 246 if components: 247 symbols = [c["symbol"] for c in components] 248 return df[df["symbol"].isin(symbols)].reset_index(drop=True) 249 except Exception: 250 pass # If index lookup fails, return unfiltered 251 252 return df 253 254 def __repr__(self) -> str: 255 return f"Screener(filters={len(self._filters)}, sector={self._sector}, index={self._index})"
A yfinance-like interface for BIST stock screening.
Data source: İş Yatırım
Examples:
import borsapy as bp screener = bp.Screener() screener.add_filter("market_cap", min=1000) # Min $1B market cap screener.add_filter("dividend_yield", min=3) # Min 3% dividend yield results = screener.run() symbol name market_cap dividend_yield 0 THYAO Türk Hava Yolları 5234.5 4.2 1 GARAN Garanti Bankası 8123.4 5.1 ...
>>> # Using templates >>> results = bp.screen_stocks(template="high_dividend") >>> # Direct filtering >>> results = bp.screen_stocks(market_cap_min=1000, pe_max=15)
55 def __init__(self): 56 """Initialize Screener.""" 57 self._provider = get_screener_provider() 58 self._filters: list[tuple[str, str, str, str]] = [] 59 self._sector: str | None = None 60 self._index: str | None = None 61 self._recommendation: str | None = None
Initialize Screener.
91 def add_filter( 92 self, 93 criteria: str, 94 min: float | None = None, 95 max: float | None = None, 96 required: bool = False, 97 ) -> "Screener": 98 """ 99 Add a filter criterion. 100 101 Args: 102 criteria: Criteria name (market_cap, pe, dividend_yield, etc.). 103 min: Minimum value. 104 max: Maximum value. 105 required: Whether this filter is required. 106 107 Returns: 108 Self for method chaining. 109 110 Examples: 111 >>> screener = Screener() 112 >>> screener.add_filter("market_cap", min=1000) 113 >>> screener.add_filter("pe", max=15) 114 """ 115 # Map criteria name to ID 116 criteria_map = self._provider.CRITERIA_MAP 117 criteria_id = criteria_map.get(criteria.lower(), criteria) 118 119 # Get default bounds for this criteria 120 defaults = self.CRITERIA_DEFAULTS.get(criteria.lower(), {"min": -999999, "max": 999999}) 121 122 # API requires both min and max - use defaults when only one is provided 123 if min is None and max is not None: 124 min = defaults["min"] 125 elif max is None and min is not None: 126 max = defaults["max"] 127 128 min_str = str(min) if min is not None else "" 129 max_str = str(max) if max is not None else "" 130 required_str = "True" if required else "False" 131 132 self._filters.append((criteria_id, min_str, max_str, required_str)) 133 return self
Add a filter criterion.
Args: criteria: Criteria name (market_cap, pe, dividend_yield, etc.). min: Minimum value. max: Maximum value. required: Whether this filter is required.
Returns: Self for method chaining.
Examples:
screener = Screener() screener.add_filter("market_cap", min=1000) screener.add_filter("pe", max=15)
135 def set_sector(self, sector: str) -> "Screener": 136 """ 137 Set sector filter. 138 139 Args: 140 sector: Sector name (e.g., "Bankacılık") or ID (e.g., "0001"). 141 142 Returns: 143 Self for method chaining. 144 """ 145 # Convert sector name to ID if needed 146 if sector and not sector.startswith("0"): 147 sectors_data = self._provider.get_sectors() 148 for s in sectors_data: 149 if s.get("name", "").lower() == sector.lower(): 150 sector = s.get("id", sector) 151 break 152 self._sector = sector 153 return self
Set sector filter.
Args: sector: Sector name (e.g., "Bankacılık") or ID (e.g., "0001").
Returns: Self for method chaining.
155 def set_index(self, index: str) -> "Screener": 156 """ 157 Set index filter. 158 159 Args: 160 index: Index name (e.g., "BIST 30", "BIST 100"). 161 162 Returns: 163 Self for method chaining. 164 """ 165 # Note: Index filtering may have limited support in the API 166 self._index = index 167 return self
Set index filter.
Args: index: Index name (e.g., "BIST 30", "BIST 100").
Returns: Self for method chaining.
169 def set_recommendation(self, recommendation: str) -> "Screener": 170 """ 171 Set recommendation filter. 172 173 Args: 174 recommendation: Recommendation type ("AL", "SAT", "TUT"). 175 176 Returns: 177 Self for method chaining. 178 """ 179 self._recommendation = recommendation.upper() 180 return self
Set recommendation filter.
Args: recommendation: Recommendation type ("AL", "SAT", "TUT").
Returns: Self for method chaining.
182 def clear(self) -> "Screener": 183 """ 184 Clear all filters. 185 186 Returns: 187 Self for method chaining. 188 """ 189 self._filters = [] 190 self._sector = None 191 self._index = None 192 self._recommendation = None 193 return self
Clear all filters.
Returns: Self for method chaining.
195 def run(self, template: str | None = None) -> pd.DataFrame: 196 """ 197 Run the screener and return results. 198 199 Args: 200 template: Optional pre-defined template to use. 201 202 Returns: 203 DataFrame with matching stocks. 204 """ 205 # Note: İş Yatırım API doesn't support index filtering directly, 206 # so we filter locally using BIST index components 207 results = self._provider.screen( 208 criterias=self._filters if self._filters else None, 209 sector=self._sector, 210 index=None, # API doesn't support this, we filter locally 211 recommendation=self._recommendation, 212 template=template, 213 ) 214 215 if not results: 216 return pd.DataFrame(columns=["symbol", "name"]) 217 218 df = pd.DataFrame(results) 219 220 # Filter by index if specified 221 if self._index and not df.empty: 222 df = self._filter_by_index(df, self._index) 223 224 return df
Run the screener and return results.
Args: template: Optional pre-defined template to use.
Returns: DataFrame with matching stocks.
782class TradingViewStream: 783 """ 784 Persistent WebSocket connection for real-time TradingView data. 785 786 Optimized for: 787 - Low latency (~50-100ms) 788 - High throughput (10-20 updates/sec) 789 - Multiple symbol subscriptions 790 - Automatic reconnection 791 792 Attributes: 793 is_connected: Whether the WebSocket is currently connected. 794 subscribed_symbols: Set of currently subscribed symbols. 795 796 Examples: 797 Basic usage:: 798 799 stream = TradingViewStream() 800 stream.connect() 801 stream.subscribe("THYAO") 802 quote = stream.get_quote("THYAO") 803 804 With callbacks:: 805 806 def on_price_update(symbol, quote): 807 print(f"{symbol}: {quote['last']}") 808 809 stream.on_quote("THYAO", on_price_update) 810 811 Context manager:: 812 813 with TradingViewStream() as stream: 814 stream.subscribe("THYAO") 815 # Trading logic... 816 """ 817 818 WS_URL = "wss://data.tradingview.com/socket.io/websocket?type=chart" 819 ORIGIN = "https://www.tradingview.com" 820 821 # Reconnection settings 822 MAX_RECONNECT_ATTEMPTS = 10 823 MAX_RECONNECT_DELAY = 30 # seconds 824 HEARTBEAT_INTERVAL = 30 # seconds 825 826 def __init__(self, auth_token: str | None = None): 827 """ 828 Initialize TradingViewStream. 829 830 Args: 831 auth_token: Optional TradingView auth token for real-time data. 832 If not provided, uses unauthorized token (~15min delay). 833 """ 834 self._ws: websocket.WebSocketApp | None = None 835 self._ws_thread: threading.Thread | None = None 836 self._heartbeat_thread: threading.Thread | None = None 837 838 # Connection state 839 self._connected = threading.Event() 840 self._should_reconnect = True 841 self._reconnect_attempts = 0 842 self._last_heartbeat_time = 0.0 843 self._heartbeat_counter = 0 844 845 # Session management 846 self._quote_session: str | None = None 847 self._chart_session: str | None = None 848 self._auth_token = auth_token 849 850 # Data storage (thread-safe) 851 self._lock = threading.RLock() 852 self._quotes: dict[str, dict[str, Any]] = {} # symbol -> latest quote 853 self._subscribed: set[str] = set() # subscribed symbols 854 self._pending_subscribes: set[str] = set() # waiting for confirmation 855 856 # Chart session data 857 # Format: {symbol: {interval: [candles]}} 858 self._chart_data: dict[str, dict[str, list[dict]]] = {} 859 self._chart_subscribed: dict[str, set[str]] = {} # symbol -> set of intervals 860 self._chart_series_counter = 0 # Counter for unique series IDs 861 self._chart_series_map: dict[str, tuple[str, str]] = {} # series_id -> (symbol, interval) 862 863 # Callbacks 864 self._callbacks: dict[str, list[Callable[[str, dict], None]]] = {} 865 self._global_callbacks: list[Callable[[str, dict], None]] = [] 866 # Chart callbacks: {f"{symbol}:{interval}": [callbacks]} 867 self._chart_callbacks: dict[str, list[Callable[[str, str, dict], None]]] = {} 868 self._global_chart_callbacks: list[Callable[[str, str, dict], None]] = [] 869 870 # Events for synchronization 871 self._quote_events: dict[str, threading.Event] = {} 872 self._chart_events: dict[str, threading.Event] = {} # f"{symbol}:{interval}" -> Event 873 874 # Pine Script studies session (lazy-loaded) 875 self._study_session: StudySession | None = None 876 877 @property 878 def is_connected(self) -> bool: 879 """Check if WebSocket is connected.""" 880 return self._connected.is_set() 881 882 @property 883 def subscribed_symbols(self) -> set[str]: 884 """Get set of currently subscribed symbols.""" 885 with self._lock: 886 return self._subscribed.copy() 887 888 def _get_auth_token(self) -> str: 889 """Get auth token from credentials or use unauthorized.""" 890 if self._auth_token: 891 return self._auth_token 892 creds = get_tradingview_auth() 893 if creds and creds.get("auth_token"): 894 return creds["auth_token"] 895 return "unauthorized_user_token" 896 897 def _generate_session_id(self, prefix: str = "qs") -> str: 898 """Generate random session ID like qs_abc123xyz.""" 899 chars = string.ascii_lowercase + string.digits 900 suffix = "".join(random.choice(chars) for _ in range(12)) 901 return f"{prefix}_{suffix}" 902 903 def _format_packet(self, data: str | dict) -> str: 904 """Format data into TradingView packet format: ~m~{length}~m~{data}""" 905 content = ( 906 json.dumps(data, separators=(",", ":")) 907 if isinstance(data, dict) 908 else data 909 ) 910 return f"~m~{len(content)}~m~{content}" 911 912 def _create_message(self, method: str, params: list) -> str: 913 """Create a TradingView message.""" 914 msg = json.dumps({"m": method, "p": params}, separators=(",", ":")) 915 return self._format_packet(msg) 916 917 def _parse_packets(self, raw: str) -> list[dict | str]: 918 """ 919 Parse TradingView packets from raw WebSocket message. 920 921 Handles both JSON packets and heartbeat packets (~h~{number}). 922 """ 923 packets: list[dict | str] = [] 924 925 # Find all packets using regex 926 # Pattern: ~m~{length}~m~{content} or ~h~{number} 927 pattern = r"~m~(\d+)~m~|~h~(\d+)" 928 929 for match in re.finditer(pattern, raw): 930 if match.group(2): # Heartbeat 931 packets.append(f"~h~{match.group(2)}") 932 elif match.group(1): # Data packet 933 length = int(match.group(1)) 934 start = match.end() 935 content = raw[start : start + length] 936 try: 937 packets.append(json.loads(content)) 938 except json.JSONDecodeError: 939 logger.warning(f"Failed to parse packet: {content[:100]}") 940 941 return packets 942 943 def _send(self, message: str) -> bool: 944 """Send message to WebSocket (thread-safe).""" 945 if self._ws and self.is_connected: 946 try: 947 self._ws.send(message) 948 return True 949 except Exception as e: 950 logger.error(f"Send error: {e}") 951 return False 952 return False 953 954 def _on_open(self, ws: websocket.WebSocketApp) -> None: 955 """Handle WebSocket connection opened.""" 956 logger.info("WebSocket connected") 957 958 # Reset reconnection counter 959 self._reconnect_attempts = 0 960 961 # Generate new sessions 962 self._quote_session = self._generate_session_id("qs") 963 self._chart_session = self._generate_session_id("cs") 964 965 # 1. Set auth token 966 auth_token = self._get_auth_token() 967 ws.send(self._create_message("set_auth_token", [auth_token])) 968 969 # 2. Create quote session 970 ws.send(self._create_message("quote_create_session", [self._quote_session])) 971 972 # 3. Set quote fields 973 ws.send( 974 self._create_message("quote_set_fields", [self._quote_session, *QUOTE_FIELDS]) 975 ) 976 977 # 4. Create chart session 978 ws.send(self._create_message("chart_create_session", [self._chart_session])) 979 980 # Mark as connected 981 self._connected.set() 982 983 # Re-subscribe to existing symbols (for reconnection) 984 with self._lock: 985 for symbol in self._subscribed: 986 self._send_subscribe(symbol) 987 988 # Re-subscribe to chart data 989 for symbol, intervals in self._chart_subscribed.items(): 990 for interval in intervals: 991 self._send_chart_subscribe(symbol, interval) 992 993 def _on_message(self, ws: websocket.WebSocketApp, message: str) -> None: 994 """Handle incoming WebSocket message.""" 995 packets = self._parse_packets(message) 996 997 for packet in packets: 998 # Handle heartbeat 999 if isinstance(packet, str) and packet.startswith("~h~"): 1000 self._handle_heartbeat(packet) 1001 continue 1002 1003 if not isinstance(packet, dict): 1004 continue 1005 1006 method = packet.get("m") 1007 params = packet.get("p", []) 1008 1009 if method == "qsd": 1010 # Quote data update 1011 self._handle_quote_data(params) 1012 1013 elif method == "quote_completed": 1014 # Initial quote load complete 1015 if len(params) >= 2: 1016 symbol = params[1] 1017 logger.debug(f"Quote completed for {symbol}") 1018 # Signal waiting threads 1019 if symbol in self._quote_events: 1020 self._quote_events[symbol].set() 1021 1022 elif method == "critical_error": 1023 logger.error(f"TradingView critical error: {params}") 1024 1025 elif method == "symbol_error": 1026 logger.warning(f"Symbol error: {params}") 1027 1028 # Chart session messages 1029 elif method == "symbol_resolved": 1030 # Symbol successfully resolved for chart 1031 self._handle_symbol_resolved(params) 1032 1033 elif method in ("timescale_update", "du"): 1034 # OHLCV data update 1035 self._handle_chart_data(params) 1036 1037 elif method == "series_error": 1038 logger.warning(f"Chart series error: {params}") 1039 1040 elif method == "series_completed": 1041 # Chart series load complete 1042 if len(params) >= 2: 1043 session_id = params[0] 1044 logger.debug(f"Series completed for session {session_id}") 1045 1046 # Pine Script study messages 1047 elif method == "study_loading": 1048 # Study is loading 1049 if len(params) >= 2: 1050 study_id = params[1] 1051 if self._study_session: 1052 self._study_session.handle_study_loading(study_id) 1053 1054 elif method == "study_completed": 1055 # Study finished loading 1056 if len(params) >= 2: 1057 study_id = params[1] 1058 if self._study_session: 1059 self._study_session.handle_study_completed(study_id) 1060 1061 elif method == "study_error": 1062 # Study error 1063 if len(params) >= 3: 1064 study_id = params[1] 1065 error = params[2] if len(params) > 2 else "Unknown error" 1066 if self._study_session: 1067 self._study_session.handle_study_error(study_id, str(error)) 1068 1069 def _handle_heartbeat(self, packet: str) -> None: 1070 """Handle heartbeat packet by echoing it back.""" 1071 self._last_heartbeat_time = time.time() 1072 # Echo heartbeat back to server 1073 self._send(self._format_packet(packet)) 1074 logger.debug(f"Heartbeat: {packet}") 1075 1076 def _handle_quote_data(self, params: list) -> None: 1077 """Handle quote data (qsd) packet.""" 1078 if len(params) < 2 or not isinstance(params[1], dict): 1079 return 1080 1081 data = params[1] 1082 symbol = data.get("n", "") # Full symbol like "BIST:THYAO" 1083 status = data.get("s") # "ok" or "error" 1084 values = data.get("v", {}) 1085 1086 if status != "ok" or not values: 1087 if status == "error": 1088 logger.warning(f"Quote error for {symbol}: {data}") 1089 return 1090 1091 # Extract base symbol (remove exchange prefix) 1092 base_symbol = symbol.split(":")[-1] if ":" in symbol else symbol 1093 1094 # Update quote cache 1095 with self._lock: 1096 if base_symbol not in self._quotes: 1097 self._quotes[base_symbol] = {} 1098 self._quotes[base_symbol].update(values) 1099 self._quotes[base_symbol]["_symbol"] = base_symbol 1100 self._quotes[base_symbol]["_full_symbol"] = symbol 1101 self._quotes[base_symbol]["_updated"] = time.time() 1102 1103 # Fire callbacks 1104 quote = self._build_quote(base_symbol) 1105 1106 # Symbol-specific callbacks 1107 for callback in self._callbacks.get(base_symbol, []): 1108 try: 1109 callback(base_symbol, quote) 1110 except Exception as e: 1111 logger.error(f"Callback error for {base_symbol}: {e}") 1112 1113 # Global callbacks 1114 for callback in self._global_callbacks: 1115 try: 1116 callback(base_symbol, quote) 1117 except Exception as e: 1118 logger.error(f"Global callback error: {e}") 1119 1120 # Signal waiting threads 1121 if base_symbol in self._quote_events: 1122 self._quote_events[base_symbol].set() 1123 1124 def _build_quote(self, symbol: str) -> dict[str, Any]: 1125 """Build standardized quote dict from raw data.""" 1126 with self._lock: 1127 raw = self._quotes.get(symbol, {}) 1128 1129 return { 1130 "symbol": symbol, 1131 "exchange": raw.get("exchange", "BIST"), 1132 "last": raw.get("lp"), 1133 "change": raw.get("ch"), 1134 "change_percent": raw.get("chp"), 1135 "open": raw.get("open_price"), 1136 "high": raw.get("high_price"), 1137 "low": raw.get("low_price"), 1138 "prev_close": raw.get("prev_close_price"), 1139 "volume": raw.get("volume"), 1140 "bid": raw.get("bid"), 1141 "ask": raw.get("ask"), 1142 "bid_size": raw.get("bid_size"), 1143 "ask_size": raw.get("ask_size"), 1144 "timestamp": raw.get("lp_time"), 1145 "description": raw.get("description"), 1146 "currency": raw.get("currency_code"), 1147 # Fundamentals 1148 "market_cap": raw.get("market_cap_basic"), 1149 "pe_ratio": raw.get("price_earnings_ttm"), 1150 "eps": raw.get("earnings_per_share_basic_ttm"), 1151 "dividend_yield": raw.get("dividends_yield"), 1152 "beta": raw.get("beta_1_year"), 1153 # 52 week 1154 "high_52_week": raw.get("high_52_week"), 1155 "low_52_week": raw.get("low_52_week"), 1156 # Meta 1157 "_updated": raw.get("_updated"), 1158 "_raw": raw, 1159 } 1160 1161 def _handle_symbol_resolved(self, params: list) -> None: 1162 """Handle symbol_resolved message for chart session.""" 1163 if len(params) < 2: 1164 return 1165 # params[0] = session_id, params[1] = symbol_info dict 1166 logger.debug(f"Symbol resolved: {params}") 1167 1168 def _handle_chart_data(self, params: list) -> None: 1169 """Handle timescale_update or du (data update) messages.""" 1170 if len(params) < 2: 1171 return 1172 1173 session_id = params[0] 1174 data = params[1] 1175 1176 if not isinstance(data, dict): 1177 return 1178 1179 # Process each series in the response 1180 for series_key, series_data in data.items(): 1181 # Handle Pine Script study data (keys like "st1", "st2", etc.) 1182 if series_key.startswith("st") and self._study_session: 1183 self._study_session.handle_study_data(series_key, series_data) 1184 continue 1185 1186 if not series_key.startswith("$prices") and series_key != "s": 1187 # Look up the series mapping 1188 if series_key not in self._chart_series_map: 1189 continue 1190 1191 # Get symbol and interval from series map or use default 1192 symbol = None 1193 interval = None 1194 1195 # Try to find series info 1196 for key, (sym, intv) in self._chart_series_map.items(): 1197 if key in str(session_id) or series_key == "$prices": 1198 symbol = sym 1199 interval = intv 1200 break 1201 1202 if not symbol or not interval: 1203 # Try to extract from the first available mapping 1204 if self._chart_series_map: 1205 first_key = next(iter(self._chart_series_map)) 1206 symbol, interval = self._chart_series_map[first_key] 1207 else: 1208 continue 1209 1210 # Extract candle data 1211 candles = [] 1212 if isinstance(series_data, dict): 1213 bars = series_data.get("s", series_data.get("st", [])) 1214 if isinstance(bars, list): 1215 for bar in bars: 1216 if isinstance(bar, dict) and "v" in bar: 1217 v = bar["v"] 1218 if len(v) >= 6: 1219 candle = { 1220 "time": int(v[0]), 1221 "open": float(v[1]), 1222 "high": float(v[2]), 1223 "low": float(v[3]), 1224 "close": float(v[4]), 1225 "volume": float(v[5]) if v[5] else 0, 1226 } 1227 candles.append(candle) 1228 1229 if candles: 1230 self._update_chart_data(symbol, interval, candles) 1231 1232 def _update_chart_data( 1233 self, symbol: str, interval: str, candles: list[dict] 1234 ) -> None: 1235 """Update chart data cache and fire callbacks.""" 1236 with self._lock: 1237 if symbol not in self._chart_data: 1238 self._chart_data[symbol] = {} 1239 if interval not in self._chart_data[symbol]: 1240 self._chart_data[symbol][interval] = [] 1241 1242 # Update or append candles 1243 existing = self._chart_data[symbol][interval] 1244 for candle in candles: 1245 # Check if we need to update the last candle or add new 1246 if existing and existing[-1]["time"] == candle["time"]: 1247 existing[-1] = candle 1248 elif not existing or candle["time"] > existing[-1]["time"]: 1249 existing.append(candle) 1250 1251 # Fire callbacks 1252 callback_key = f"{symbol}:{interval}" 1253 latest_candle = candles[-1] if candles else None 1254 1255 if latest_candle: 1256 # Symbol-specific callbacks 1257 for callback in self._chart_callbacks.get(callback_key, []): 1258 try: 1259 callback(symbol, interval, latest_candle) 1260 except Exception as e: 1261 logger.error(f"Chart callback error for {callback_key}: {e}") 1262 1263 # Global chart callbacks 1264 for callback in self._global_chart_callbacks: 1265 try: 1266 callback(symbol, interval, latest_candle) 1267 except Exception as e: 1268 logger.error(f"Global chart callback error: {e}") 1269 1270 # Signal waiting threads 1271 if callback_key in self._chart_events: 1272 self._chart_events[callback_key].set() 1273 1274 def _send_chart_subscribe(self, symbol: str, interval: str, exchange: str = "BIST") -> None: 1275 """Send chart subscription messages.""" 1276 tv_interval = CHART_TIMEFRAMES.get(interval, interval) 1277 tv_symbol = f"{exchange}:{symbol}" 1278 1279 # Generate unique series ID 1280 self._chart_series_counter += 1 1281 series_id = f"ser_{self._chart_series_counter}" 1282 1283 # Store mapping 1284 self._chart_series_map[series_id] = (symbol, interval) 1285 1286 # 1. Resolve symbol with configuration 1287 symbol_config = json.dumps({ 1288 "symbol": tv_symbol, 1289 "adjustment": "splits", 1290 "session": "regular", 1291 }) 1292 self._send( 1293 self._create_message( 1294 "resolve_symbol", 1295 [self._chart_session, series_id, f"={symbol_config}"], 1296 ) 1297 ) 1298 1299 # 2. Create series for OHLCV data 1300 self._send( 1301 self._create_message( 1302 "create_series", 1303 [ 1304 self._chart_session, 1305 "$prices", # Fixed price stream ID 1306 "s1", # Series index 1307 series_id, # Reference to resolved symbol 1308 tv_interval, # Timeframe 1309 300, # Number of bars 1310 ], 1311 ) 1312 ) 1313 1314 def _send_chart_unsubscribe(self, symbol: str, interval: str) -> None: 1315 """Send chart unsubscription messages.""" 1316 # Find and remove series 1317 series_to_remove = None 1318 for series_id, (sym, intv) in list(self._chart_series_map.items()): 1319 if sym == symbol and intv == interval: 1320 series_to_remove = series_id 1321 break 1322 1323 if series_to_remove: 1324 self._send( 1325 self._create_message( 1326 "remove_series", [self._chart_session, "$prices"] 1327 ) 1328 ) 1329 del self._chart_series_map[series_to_remove] 1330 1331 def _on_error(self, ws: websocket.WebSocketApp, error: Exception) -> None: 1332 """Handle WebSocket error.""" 1333 logger.error(f"WebSocket error: {error}") 1334 1335 def _on_close( 1336 self, 1337 ws: websocket.WebSocketApp, 1338 close_status: int | None, 1339 close_msg: str | None, 1340 ) -> None: 1341 """Handle WebSocket close.""" 1342 logger.info(f"WebSocket closed: {close_status} - {close_msg}") 1343 self._connected.clear() 1344 1345 # Attempt reconnection if needed 1346 if self._should_reconnect: 1347 self._reconnect() 1348 1349 def _reconnect(self) -> None: 1350 """Reconnect with exponential backoff.""" 1351 if self._reconnect_attempts >= self.MAX_RECONNECT_ATTEMPTS: 1352 logger.error("Max reconnection attempts reached") 1353 return 1354 1355 # Calculate delay with exponential backoff 1356 delay = min(self.MAX_RECONNECT_DELAY, 2 ** self._reconnect_attempts) 1357 logger.info(f"Reconnecting in {delay}s (attempt {self._reconnect_attempts + 1})") 1358 1359 self._reconnect_attempts += 1 1360 time.sleep(delay) 1361 1362 # Reconnect 1363 self._start_websocket() 1364 1365 def _start_websocket(self) -> None: 1366 """Start WebSocket connection in background thread.""" 1367 self._ws = websocket.WebSocketApp( 1368 f"{self.WS_URL}?type=chart", 1369 on_open=self._on_open, 1370 on_message=self._on_message, 1371 on_error=self._on_error, 1372 on_close=self._on_close, 1373 header={"Origin": self.ORIGIN}, 1374 ) 1375 1376 self._ws_thread = threading.Thread( 1377 target=self._ws.run_forever, 1378 kwargs={"ping_interval": 0}, # We handle heartbeat ourselves 1379 daemon=True, 1380 ) 1381 self._ws_thread.start() 1382 1383 def _send_subscribe(self, symbol: str, exchange: str = "BIST") -> None: 1384 """Send subscribe message for symbol.""" 1385 tv_symbol = f"{exchange}:{symbol}" 1386 self._send( 1387 self._create_message( 1388 "quote_add_symbols", [self._quote_session, tv_symbol] 1389 ) 1390 ) 1391 1392 def _send_unsubscribe(self, symbol: str, exchange: str = "BIST") -> None: 1393 """Send unsubscribe message for symbol.""" 1394 tv_symbol = f"{exchange}:{symbol}" 1395 self._send( 1396 self._create_message( 1397 "quote_remove_symbols", [self._quote_session, tv_symbol] 1398 ) 1399 ) 1400 1401 # Public API 1402 1403 def connect(self, timeout: float = 10.0) -> bool: 1404 """ 1405 Establish persistent WebSocket connection. 1406 1407 Args: 1408 timeout: Maximum time to wait for connection in seconds. 1409 1410 Returns: 1411 True if connected successfully, False otherwise. 1412 1413 Raises: 1414 TimeoutError: If connection times out. 1415 1416 Example: 1417 >>> stream = TradingViewStream() 1418 >>> stream.connect() 1419 >>> print(stream.is_connected) 1420 True 1421 """ 1422 if self.is_connected: 1423 return True 1424 1425 self._should_reconnect = True 1426 self._start_websocket() 1427 1428 # Wait for connection 1429 if not self._connected.wait(timeout=timeout): 1430 raise TimeoutError(f"Connection timed out after {timeout}s") 1431 1432 return True 1433 1434 def disconnect(self) -> None: 1435 """ 1436 Close WebSocket connection and cleanup. 1437 1438 Example: 1439 >>> stream.disconnect() 1440 >>> print(stream.is_connected) 1441 False 1442 """ 1443 self._should_reconnect = False 1444 self._connected.clear() 1445 1446 if self._ws: 1447 self._ws.close() 1448 self._ws = None 1449 1450 if self._ws_thread and self._ws_thread.is_alive(): 1451 self._ws_thread.join(timeout=2) 1452 1453 # Clear state 1454 with self._lock: 1455 # Quote state 1456 self._quotes.clear() 1457 self._subscribed.clear() 1458 self._callbacks.clear() 1459 self._global_callbacks.clear() 1460 self._quote_events.clear() 1461 1462 # Chart state 1463 self._chart_data.clear() 1464 self._chart_subscribed.clear() 1465 self._chart_series_map.clear() 1466 self._chart_callbacks.clear() 1467 self._global_chart_callbacks.clear() 1468 self._chart_events.clear() 1469 1470 logger.info("Disconnected") 1471 1472 def subscribe(self, symbol: str, exchange: str = "BIST") -> None: 1473 """ 1474 Subscribe to symbol updates. 1475 1476 Args: 1477 symbol: Stock symbol (e.g., "THYAO", "GARAN") 1478 exchange: Exchange name (default: "BIST") 1479 1480 Example: 1481 >>> stream.subscribe("THYAO") 1482 >>> stream.subscribe("GARAN") 1483 """ 1484 symbol = symbol.upper() 1485 1486 with self._lock: 1487 if symbol in self._subscribed: 1488 return 1489 self._subscribed.add(symbol) 1490 self._quote_events[symbol] = threading.Event() 1491 1492 if self.is_connected: 1493 self._send_subscribe(symbol, exchange) 1494 1495 def unsubscribe(self, symbol: str, exchange: str = "BIST") -> None: 1496 """ 1497 Unsubscribe from symbol. 1498 1499 Args: 1500 symbol: Stock symbol 1501 exchange: Exchange name 1502 1503 Example: 1504 >>> stream.unsubscribe("THYAO") 1505 """ 1506 symbol = symbol.upper() 1507 1508 with self._lock: 1509 self._subscribed.discard(symbol) 1510 self._quotes.pop(symbol, None) 1511 self._callbacks.pop(symbol, None) 1512 self._quote_events.pop(symbol, None) 1513 1514 if self.is_connected: 1515 self._send_unsubscribe(symbol, exchange) 1516 1517 # Chart Session API 1518 1519 def subscribe_chart( 1520 self, symbol: str, interval: str = "1m", exchange: str = "BIST" 1521 ) -> None: 1522 """ 1523 Subscribe to OHLCV candle updates for a symbol. 1524 1525 Args: 1526 symbol: Stock symbol (e.g., "THYAO", "GARAN") 1527 interval: Candle interval. Valid values: 1528 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d, 1wk, 1mo 1529 exchange: Exchange name (default: "BIST") 1530 1531 Example: 1532 >>> stream.subscribe_chart("THYAO", "1m") 1533 >>> stream.subscribe_chart("GARAN", "1h") 1534 """ 1535 symbol = symbol.upper() 1536 interval = interval.lower() 1537 1538 # Validate interval 1539 if interval not in CHART_TIMEFRAMES: 1540 raise ValueError( 1541 f"Invalid interval '{interval}'. " 1542 f"Valid intervals: {list(CHART_TIMEFRAMES.keys())}" 1543 ) 1544 1545 with self._lock: 1546 if symbol not in self._chart_subscribed: 1547 self._chart_subscribed[symbol] = set() 1548 1549 if interval in self._chart_subscribed[symbol]: 1550 return # Already subscribed 1551 1552 self._chart_subscribed[symbol].add(interval) 1553 1554 # Initialize data storage 1555 if symbol not in self._chart_data: 1556 self._chart_data[symbol] = {} 1557 if interval not in self._chart_data[symbol]: 1558 self._chart_data[symbol][interval] = [] 1559 1560 # Create event for this subscription 1561 event_key = f"{symbol}:{interval}" 1562 self._chart_events[event_key] = threading.Event() 1563 1564 if self.is_connected: 1565 self._send_chart_subscribe(symbol, interval, exchange) 1566 1567 def unsubscribe_chart( 1568 self, symbol: str, interval: str, exchange: str = "BIST" 1569 ) -> None: 1570 """ 1571 Unsubscribe from chart updates. 1572 1573 Args: 1574 symbol: Stock symbol 1575 interval: Candle interval 1576 exchange: Exchange name 1577 1578 Example: 1579 >>> stream.unsubscribe_chart("THYAO", "1m") 1580 """ 1581 symbol = symbol.upper() 1582 interval = interval.lower() 1583 1584 with self._lock: 1585 if symbol in self._chart_subscribed: 1586 self._chart_subscribed[symbol].discard(interval) 1587 if not self._chart_subscribed[symbol]: 1588 del self._chart_subscribed[symbol] 1589 1590 if symbol in self._chart_data and interval in self._chart_data[symbol]: 1591 del self._chart_data[symbol][interval] 1592 if not self._chart_data[symbol]: 1593 del self._chart_data[symbol] 1594 1595 event_key = f"{symbol}:{interval}" 1596 self._chart_events.pop(event_key, None) 1597 self._chart_callbacks.pop(event_key, None) 1598 1599 if self.is_connected: 1600 self._send_chart_unsubscribe(symbol, interval) 1601 1602 def get_candle(self, symbol: str, interval: str) -> dict[str, Any] | None: 1603 """ 1604 Get latest cached candle (instant, ~1ms). 1605 1606 Args: 1607 symbol: Stock symbol 1608 interval: Candle interval 1609 1610 Returns: 1611 Candle dict or None if not subscribed/no data yet. 1612 Candle format: 1613 { 1614 "time": 1737123456, # Unix timestamp 1615 "open": 285.0, # Open price 1616 "high": 286.5, # High price 1617 "low": 284.0, # Low price 1618 "close": 285.5, # Close price 1619 "volume": 123456 # Volume 1620 } 1621 1622 Example: 1623 >>> candle = stream.get_candle("THYAO", "1m") 1624 >>> print(candle['close']) 1625 285.5 1626 """ 1627 symbol = symbol.upper() 1628 interval = interval.lower() 1629 1630 with self._lock: 1631 if symbol not in self._chart_data: 1632 return None 1633 if interval not in self._chart_data[symbol]: 1634 return None 1635 candles = self._chart_data[symbol][interval] 1636 if not candles: 1637 return None 1638 return candles[-1].copy() 1639 1640 def get_candles( 1641 self, symbol: str, interval: str, count: int | None = None 1642 ) -> list[dict[str, Any]]: 1643 """ 1644 Get cached candles. 1645 1646 Args: 1647 symbol: Stock symbol 1648 interval: Candle interval 1649 count: Number of candles to return (None = all) 1650 1651 Returns: 1652 List of candle dicts, oldest first. 1653 1654 Example: 1655 >>> candles = stream.get_candles("THYAO", "1m", count=10) 1656 >>> print(len(candles)) 1657 10 1658 """ 1659 symbol = symbol.upper() 1660 interval = interval.lower() 1661 1662 with self._lock: 1663 if symbol not in self._chart_data: 1664 return [] 1665 if interval not in self._chart_data[symbol]: 1666 return [] 1667 candles = self._chart_data[symbol][interval] 1668 if count: 1669 return [c.copy() for c in candles[-count:]] 1670 return [c.copy() for c in candles] 1671 1672 def wait_for_candle( 1673 self, symbol: str, interval: str, timeout: float = 5.0 1674 ) -> dict[str, Any]: 1675 """ 1676 Wait for first candle (blocking). 1677 1678 Useful after subscribing to ensure data is received. 1679 1680 Args: 1681 symbol: Stock symbol 1682 interval: Candle interval 1683 timeout: Maximum wait time in seconds 1684 1685 Returns: 1686 Candle dict 1687 1688 Raises: 1689 TimeoutError: If candle not received within timeout. 1690 1691 Example: 1692 >>> stream.subscribe_chart("THYAO", "1m") 1693 >>> candle = stream.wait_for_candle("THYAO", "1m") 1694 """ 1695 symbol = symbol.upper() 1696 interval = interval.lower() 1697 1698 # Check if already have data 1699 candle = self.get_candle(symbol, interval) 1700 if candle: 1701 return candle 1702 1703 # Wait for data 1704 event_key = f"{symbol}:{interval}" 1705 with self._lock: 1706 if event_key not in self._chart_events: 1707 self._chart_events[event_key] = threading.Event() 1708 event = self._chart_events[event_key] 1709 event.clear() 1710 1711 if not event.wait(timeout=timeout): 1712 raise TimeoutError(f"Timeout waiting for candle: {symbol} {interval}") 1713 1714 candle = self.get_candle(symbol, interval) 1715 if candle is None: 1716 raise TimeoutError(f"No candle data received for {symbol} {interval}") 1717 return candle 1718 1719 def on_candle( 1720 self, 1721 symbol: str, 1722 interval: str, 1723 callback: Callable[[str, str, dict], None], 1724 ) -> None: 1725 """ 1726 Register callback for candle updates. 1727 1728 Callback signature: callback(symbol: str, interval: str, candle: dict) 1729 1730 Args: 1731 symbol: Stock symbol 1732 interval: Candle interval 1733 callback: Function to call on each update 1734 1735 Example: 1736 >>> def on_candle_update(symbol, interval, candle): 1737 ... print(f"{symbol} {interval}: O={candle['open']} C={candle['close']}") 1738 >>> stream.on_candle("THYAO", "1m", on_candle_update) 1739 """ 1740 symbol = symbol.upper() 1741 interval = interval.lower() 1742 callback_key = f"{symbol}:{interval}" 1743 1744 with self._lock: 1745 if callback_key not in self._chart_callbacks: 1746 self._chart_callbacks[callback_key] = [] 1747 self._chart_callbacks[callback_key].append(callback) 1748 1749 def on_any_candle( 1750 self, callback: Callable[[str, str, dict], None] 1751 ) -> None: 1752 """ 1753 Register callback for all candle updates. 1754 1755 Args: 1756 callback: Function to call on each update for any subscription. 1757 1758 Example: 1759 >>> def on_any_update(symbol, interval, candle): 1760 ... print(f"{symbol} {interval}: {candle['close']}") 1761 >>> stream.on_any_candle(on_any_update) 1762 """ 1763 self._global_chart_callbacks.append(callback) 1764 1765 def remove_candle_callback( 1766 self, 1767 symbol: str, 1768 interval: str, 1769 callback: Callable[[str, str, dict], None], 1770 ) -> None: 1771 """ 1772 Remove a registered candle callback. 1773 1774 Args: 1775 symbol: Stock symbol 1776 interval: Candle interval 1777 callback: The callback to remove 1778 """ 1779 symbol = symbol.upper() 1780 interval = interval.lower() 1781 callback_key = f"{symbol}:{interval}" 1782 1783 with self._lock: 1784 if callback_key in self._chart_callbacks: 1785 try: 1786 self._chart_callbacks[callback_key].remove(callback) 1787 except ValueError: 1788 pass 1789 1790 @property 1791 def chart_subscriptions(self) -> dict[str, set[str]]: 1792 """Get current chart subscriptions. 1793 1794 Returns: 1795 Dict mapping symbol to set of subscribed intervals. 1796 """ 1797 with self._lock: 1798 return { 1799 sym: intervals.copy() 1800 for sym, intervals in self._chart_subscribed.items() 1801 } 1802 1803 def get_quote(self, symbol: str) -> dict[str, Any] | None: 1804 """ 1805 Get latest cached quote (instant, ~1ms). 1806 1807 Args: 1808 symbol: Stock symbol 1809 1810 Returns: 1811 Quote dict or None if not subscribed/no data yet. 1812 1813 Example: 1814 >>> quote = stream.get_quote("THYAO") 1815 >>> print(quote['last']) 1816 299.0 1817 """ 1818 symbol = symbol.upper() 1819 with self._lock: 1820 if symbol not in self._quotes: 1821 return None 1822 return self._build_quote(symbol) 1823 1824 def wait_for_quote( 1825 self, symbol: str, timeout: float = 5.0 1826 ) -> dict[str, Any]: 1827 """ 1828 Wait for first quote (blocking). 1829 1830 Useful after subscribing to ensure data is received. 1831 1832 Args: 1833 symbol: Stock symbol 1834 timeout: Maximum wait time in seconds 1835 1836 Returns: 1837 Quote dict 1838 1839 Raises: 1840 TimeoutError: If quote not received within timeout. 1841 1842 Example: 1843 >>> stream.subscribe("THYAO") 1844 >>> quote = stream.wait_for_quote("THYAO") 1845 """ 1846 symbol = symbol.upper() 1847 1848 # Check if already have data 1849 quote = self.get_quote(symbol) 1850 if quote and quote.get("last") is not None: 1851 return quote 1852 1853 # Wait for data 1854 with self._lock: 1855 if symbol not in self._quote_events: 1856 self._quote_events[symbol] = threading.Event() 1857 event = self._quote_events[symbol] 1858 event.clear() 1859 1860 if not event.wait(timeout=timeout): 1861 raise TimeoutError(f"Timeout waiting for quote: {symbol}") 1862 1863 quote = self.get_quote(symbol) 1864 if quote is None: 1865 raise TimeoutError(f"No quote data received for {symbol}") 1866 return quote 1867 1868 def on_quote( 1869 self, symbol: str, callback: Callable[[str, dict], None] 1870 ) -> None: 1871 """ 1872 Register callback for quote updates. 1873 1874 Callback signature: callback(symbol: str, quote: dict) 1875 1876 Args: 1877 symbol: Stock symbol 1878 callback: Function to call on each update 1879 1880 Example: 1881 >>> def on_price_update(symbol, quote): 1882 ... print(f"{symbol}: {quote['last']}") 1883 >>> stream.on_quote("THYAO", on_price_update) 1884 """ 1885 symbol = symbol.upper() 1886 with self._lock: 1887 if symbol not in self._callbacks: 1888 self._callbacks[symbol] = [] 1889 self._callbacks[symbol].append(callback) 1890 1891 def on_any_quote(self, callback: Callable[[str, dict], None]) -> None: 1892 """ 1893 Register callback for all quote updates. 1894 1895 Args: 1896 callback: Function to call on each update for any symbol. 1897 1898 Example: 1899 >>> def on_any_update(symbol, quote): 1900 ... print(f"{symbol}: {quote['last']}") 1901 >>> stream.on_any_quote(on_any_update) 1902 """ 1903 self._global_callbacks.append(callback) 1904 1905 def remove_callback( 1906 self, symbol: str, callback: Callable[[str, dict], None] 1907 ) -> None: 1908 """ 1909 Remove a registered callback. 1910 1911 Args: 1912 symbol: Stock symbol 1913 callback: The callback to remove 1914 """ 1915 symbol = symbol.upper() 1916 with self._lock: 1917 if symbol in self._callbacks: 1918 try: 1919 self._callbacks[symbol].remove(callback) 1920 except ValueError: 1921 pass 1922 1923 def wait(self) -> None: 1924 """ 1925 Block until disconnect. 1926 1927 Useful for keeping the stream alive in main thread. 1928 1929 Example: 1930 >>> stream.connect() 1931 >>> stream.subscribe("THYAO") 1932 >>> stream.on_quote("THYAO", my_callback) 1933 >>> stream.wait() # Blocks forever 1934 """ 1935 if self._ws_thread: 1936 self._ws_thread.join() 1937 1938 # Context manager 1939 1940 def __enter__(self) -> TradingViewStream: 1941 """Context manager entry.""" 1942 self.connect() 1943 return self 1944 1945 def __exit__(self, exc_type, exc_val, exc_tb) -> None: 1946 """Context manager exit.""" 1947 self.disconnect() 1948 1949 # Pine Script Study API 1950 1951 @property 1952 def studies(self) -> StudySession: 1953 """ 1954 Access Pine Script study session. 1955 1956 Returns: 1957 StudySession instance for managing indicators. 1958 1959 Example: 1960 >>> stream.studies.add("THYAO", "1m", "RSI") 1961 >>> stream.studies.get("THYAO", "1m", "RSI") 1962 """ 1963 if self._study_session is None: 1964 self._study_session = StudySession(self) 1965 return self._study_session 1966 1967 def add_study( 1968 self, 1969 symbol: str, 1970 interval: str, 1971 indicator: str, 1972 **kwargs: Any, 1973 ) -> str: 1974 """ 1975 Add a Pine indicator study (convenience method). 1976 1977 Args: 1978 symbol: Stock symbol (e.g., "THYAO") 1979 interval: Chart interval (e.g., "1m", "1d") 1980 indicator: Indicator name. Examples: 1981 - "RSI", "MACD", "BB" (standard) 1982 - "STD;RSI" (full standard ID) 1983 - "PUB;abc123" (community indicator) 1984 **kwargs: Indicator-specific parameters (e.g., length=14) 1985 1986 Returns: 1987 Study ID 1988 1989 Example: 1990 >>> stream.add_study("THYAO", "1m", "RSI") 1991 >>> stream.add_study("THYAO", "1m", "MACD") 1992 """ 1993 return self.studies.add(symbol, interval, indicator, **kwargs) 1994 1995 def remove_study(self, symbol: str, interval: str, indicator: str) -> None: 1996 """ 1997 Remove a Pine indicator study (convenience method). 1998 1999 Args: 2000 symbol: Stock symbol 2001 interval: Chart interval 2002 indicator: Indicator name 2003 2004 Example: 2005 >>> stream.remove_study("THYAO", "1m", "RSI") 2006 """ 2007 self.studies.remove(symbol, interval, indicator) 2008 2009 def get_study( 2010 self, symbol: str, interval: str, indicator: str 2011 ) -> dict[str, Any] | None: 2012 """ 2013 Get latest study values (convenience method). 2014 2015 Args: 2016 symbol: Stock symbol 2017 interval: Chart interval 2018 indicator: Indicator name 2019 2020 Returns: 2021 Dict of indicator values or None. 2022 2023 Example: 2024 >>> rsi = stream.get_study("THYAO", "1m", "RSI") 2025 >>> print(rsi['value']) 2026 48.5 2027 """ 2028 return self.studies.get(symbol, interval, indicator) 2029 2030 def get_studies(self, symbol: str, interval: str) -> dict[str, dict[str, Any]]: 2031 """ 2032 Get all study values for a symbol/interval (convenience method). 2033 2034 Args: 2035 symbol: Stock symbol 2036 interval: Chart interval 2037 2038 Returns: 2039 Dict mapping indicator name to values. 2040 2041 Example: 2042 >>> studies = stream.get_studies("THYAO", "1m") 2043 >>> print(studies['RSI']['value']) 2044 48.5 2045 """ 2046 return self.studies.get_all(symbol, interval) 2047 2048 def on_study( 2049 self, 2050 symbol: str, 2051 interval: str, 2052 indicator: str, 2053 callback: Callable[[str, str, str, dict], None], 2054 ) -> None: 2055 """ 2056 Register callback for study updates (convenience method). 2057 2058 Callback signature: callback(symbol, interval, indicator, values) 2059 2060 Args: 2061 symbol: Stock symbol 2062 interval: Chart interval 2063 indicator: Indicator name 2064 callback: Function to call on each update 2065 2066 Example: 2067 >>> def on_rsi(symbol, interval, indicator, values): 2068 ... print(f"{symbol} RSI: {values['value']}") 2069 >>> stream.on_study("THYAO", "1m", "RSI", on_rsi) 2070 """ 2071 self.studies.on_update(symbol, interval, indicator, callback) 2072 2073 def on_any_study( 2074 self, callback: Callable[[str, str, str, dict], None] 2075 ) -> None: 2076 """ 2077 Register callback for any study update (convenience method). 2078 2079 Args: 2080 callback: Function to call on each update. 2081 2082 Example: 2083 >>> stream.on_any_study( 2084 ... lambda s, i, n, v: print(f"{s} {n}: {v}") 2085 ... ) 2086 """ 2087 self.studies.on_any_update(callback) 2088 2089 def wait_for_study( 2090 self, symbol: str, interval: str, indicator: str, timeout: float = 10.0 2091 ) -> dict[str, Any]: 2092 """ 2093 Wait for study data (blocking convenience method). 2094 2095 Args: 2096 symbol: Stock symbol 2097 interval: Chart interval 2098 indicator: Indicator name 2099 timeout: Maximum wait time in seconds 2100 2101 Returns: 2102 Study values dict 2103 2104 Raises: 2105 TimeoutError: If data not received within timeout 2106 2107 Example: 2108 >>> stream.add_study("THYAO", "1m", "RSI") 2109 >>> rsi = stream.wait_for_study("THYAO", "1m", "RSI") 2110 """ 2111 return self.studies.wait_for(symbol, interval, indicator, timeout) 2112 2113 # Utility methods 2114 2115 def get_all_quotes(self) -> dict[str, dict[str, Any]]: 2116 """ 2117 Get all cached quotes. 2118 2119 Returns: 2120 Dict mapping symbol to quote dict. 2121 """ 2122 with self._lock: 2123 return {sym: self._build_quote(sym) for sym in self._quotes} 2124 2125 def ping(self) -> float: 2126 """ 2127 Measure round-trip latency. 2128 2129 Returns: 2130 Latency in milliseconds. 2131 """ 2132 if not self.is_connected: 2133 return -1 2134 2135 start = time.time() 2136 # Send a heartbeat-like ping 2137 self._send(self._format_packet(f"~h~{self._heartbeat_counter}")) 2138 self._heartbeat_counter += 1 2139 elapsed = (time.time() - start) * 1000 2140 return elapsed
Persistent WebSocket connection for real-time TradingView data.
Optimized for:
- Low latency (~50-100ms)
- High throughput (10-20 updates/sec)
- Multiple symbol subscriptions
- Automatic reconnection
Attributes: is_connected: Whether the WebSocket is currently connected. subscribed_symbols: Set of currently subscribed symbols.
Examples: Basic usage::
stream = TradingViewStream()
stream.connect()
stream.subscribe("THYAO")
quote = stream.get_quote("THYAO")
With callbacks::
def on_price_update(symbol, quote):
print(f"{symbol}: {quote['last']}")
stream.on_quote("THYAO", on_price_update)
Context manager::
with TradingViewStream() as stream:
stream.subscribe("THYAO")
# Trading logic...
826 def __init__(self, auth_token: str | None = None): 827 """ 828 Initialize TradingViewStream. 829 830 Args: 831 auth_token: Optional TradingView auth token for real-time data. 832 If not provided, uses unauthorized token (~15min delay). 833 """ 834 self._ws: websocket.WebSocketApp | None = None 835 self._ws_thread: threading.Thread | None = None 836 self._heartbeat_thread: threading.Thread | None = None 837 838 # Connection state 839 self._connected = threading.Event() 840 self._should_reconnect = True 841 self._reconnect_attempts = 0 842 self._last_heartbeat_time = 0.0 843 self._heartbeat_counter = 0 844 845 # Session management 846 self._quote_session: str | None = None 847 self._chart_session: str | None = None 848 self._auth_token = auth_token 849 850 # Data storage (thread-safe) 851 self._lock = threading.RLock() 852 self._quotes: dict[str, dict[str, Any]] = {} # symbol -> latest quote 853 self._subscribed: set[str] = set() # subscribed symbols 854 self._pending_subscribes: set[str] = set() # waiting for confirmation 855 856 # Chart session data 857 # Format: {symbol: {interval: [candles]}} 858 self._chart_data: dict[str, dict[str, list[dict]]] = {} 859 self._chart_subscribed: dict[str, set[str]] = {} # symbol -> set of intervals 860 self._chart_series_counter = 0 # Counter for unique series IDs 861 self._chart_series_map: dict[str, tuple[str, str]] = {} # series_id -> (symbol, interval) 862 863 # Callbacks 864 self._callbacks: dict[str, list[Callable[[str, dict], None]]] = {} 865 self._global_callbacks: list[Callable[[str, dict], None]] = [] 866 # Chart callbacks: {f"{symbol}:{interval}": [callbacks]} 867 self._chart_callbacks: dict[str, list[Callable[[str, str, dict], None]]] = {} 868 self._global_chart_callbacks: list[Callable[[str, str, dict], None]] = [] 869 870 # Events for synchronization 871 self._quote_events: dict[str, threading.Event] = {} 872 self._chart_events: dict[str, threading.Event] = {} # f"{symbol}:{interval}" -> Event 873 874 # Pine Script studies session (lazy-loaded) 875 self._study_session: StudySession | None = None
Initialize TradingViewStream.
Args: auth_token: Optional TradingView auth token for real-time data. If not provided, uses unauthorized token (~15min delay).
877 @property 878 def is_connected(self) -> bool: 879 """Check if WebSocket is connected.""" 880 return self._connected.is_set()
Check if WebSocket is connected.
882 @property 883 def subscribed_symbols(self) -> set[str]: 884 """Get set of currently subscribed symbols.""" 885 with self._lock: 886 return self._subscribed.copy()
Get set of currently subscribed symbols.
1403 def connect(self, timeout: float = 10.0) -> bool: 1404 """ 1405 Establish persistent WebSocket connection. 1406 1407 Args: 1408 timeout: Maximum time to wait for connection in seconds. 1409 1410 Returns: 1411 True if connected successfully, False otherwise. 1412 1413 Raises: 1414 TimeoutError: If connection times out. 1415 1416 Example: 1417 >>> stream = TradingViewStream() 1418 >>> stream.connect() 1419 >>> print(stream.is_connected) 1420 True 1421 """ 1422 if self.is_connected: 1423 return True 1424 1425 self._should_reconnect = True 1426 self._start_websocket() 1427 1428 # Wait for connection 1429 if not self._connected.wait(timeout=timeout): 1430 raise TimeoutError(f"Connection timed out after {timeout}s") 1431 1432 return True
Establish persistent WebSocket connection.
Args: timeout: Maximum time to wait for connection in seconds.
Returns: True if connected successfully, False otherwise.
Raises: TimeoutError: If connection times out.
Example:
stream = TradingViewStream() stream.connect() print(stream.is_connected) True
1434 def disconnect(self) -> None: 1435 """ 1436 Close WebSocket connection and cleanup. 1437 1438 Example: 1439 >>> stream.disconnect() 1440 >>> print(stream.is_connected) 1441 False 1442 """ 1443 self._should_reconnect = False 1444 self._connected.clear() 1445 1446 if self._ws: 1447 self._ws.close() 1448 self._ws = None 1449 1450 if self._ws_thread and self._ws_thread.is_alive(): 1451 self._ws_thread.join(timeout=2) 1452 1453 # Clear state 1454 with self._lock: 1455 # Quote state 1456 self._quotes.clear() 1457 self._subscribed.clear() 1458 self._callbacks.clear() 1459 self._global_callbacks.clear() 1460 self._quote_events.clear() 1461 1462 # Chart state 1463 self._chart_data.clear() 1464 self._chart_subscribed.clear() 1465 self._chart_series_map.clear() 1466 self._chart_callbacks.clear() 1467 self._global_chart_callbacks.clear() 1468 self._chart_events.clear() 1469 1470 logger.info("Disconnected")
Close WebSocket connection and cleanup.
Example:
stream.disconnect() print(stream.is_connected) False
1472 def subscribe(self, symbol: str, exchange: str = "BIST") -> None: 1473 """ 1474 Subscribe to symbol updates. 1475 1476 Args: 1477 symbol: Stock symbol (e.g., "THYAO", "GARAN") 1478 exchange: Exchange name (default: "BIST") 1479 1480 Example: 1481 >>> stream.subscribe("THYAO") 1482 >>> stream.subscribe("GARAN") 1483 """ 1484 symbol = symbol.upper() 1485 1486 with self._lock: 1487 if symbol in self._subscribed: 1488 return 1489 self._subscribed.add(symbol) 1490 self._quote_events[symbol] = threading.Event() 1491 1492 if self.is_connected: 1493 self._send_subscribe(symbol, exchange)
Subscribe to symbol updates.
Args: symbol: Stock symbol (e.g., "THYAO", "GARAN") exchange: Exchange name (default: "BIST")
Example:
stream.subscribe("THYAO") stream.subscribe("GARAN")
1495 def unsubscribe(self, symbol: str, exchange: str = "BIST") -> None: 1496 """ 1497 Unsubscribe from symbol. 1498 1499 Args: 1500 symbol: Stock symbol 1501 exchange: Exchange name 1502 1503 Example: 1504 >>> stream.unsubscribe("THYAO") 1505 """ 1506 symbol = symbol.upper() 1507 1508 with self._lock: 1509 self._subscribed.discard(symbol) 1510 self._quotes.pop(symbol, None) 1511 self._callbacks.pop(symbol, None) 1512 self._quote_events.pop(symbol, None) 1513 1514 if self.is_connected: 1515 self._send_unsubscribe(symbol, exchange)
Unsubscribe from symbol.
Args: symbol: Stock symbol exchange: Exchange name
Example:
stream.unsubscribe("THYAO")
1519 def subscribe_chart( 1520 self, symbol: str, interval: str = "1m", exchange: str = "BIST" 1521 ) -> None: 1522 """ 1523 Subscribe to OHLCV candle updates for a symbol. 1524 1525 Args: 1526 symbol: Stock symbol (e.g., "THYAO", "GARAN") 1527 interval: Candle interval. Valid values: 1528 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d, 1wk, 1mo 1529 exchange: Exchange name (default: "BIST") 1530 1531 Example: 1532 >>> stream.subscribe_chart("THYAO", "1m") 1533 >>> stream.subscribe_chart("GARAN", "1h") 1534 """ 1535 symbol = symbol.upper() 1536 interval = interval.lower() 1537 1538 # Validate interval 1539 if interval not in CHART_TIMEFRAMES: 1540 raise ValueError( 1541 f"Invalid interval '{interval}'. " 1542 f"Valid intervals: {list(CHART_TIMEFRAMES.keys())}" 1543 ) 1544 1545 with self._lock: 1546 if symbol not in self._chart_subscribed: 1547 self._chart_subscribed[symbol] = set() 1548 1549 if interval in self._chart_subscribed[symbol]: 1550 return # Already subscribed 1551 1552 self._chart_subscribed[symbol].add(interval) 1553 1554 # Initialize data storage 1555 if symbol not in self._chart_data: 1556 self._chart_data[symbol] = {} 1557 if interval not in self._chart_data[symbol]: 1558 self._chart_data[symbol][interval] = [] 1559 1560 # Create event for this subscription 1561 event_key = f"{symbol}:{interval}" 1562 self._chart_events[event_key] = threading.Event() 1563 1564 if self.is_connected: 1565 self._send_chart_subscribe(symbol, interval, exchange)
Subscribe to OHLCV candle updates for a symbol.
Args: symbol: Stock symbol (e.g., "THYAO", "GARAN") interval: Candle interval. Valid values: 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1d, 1wk, 1mo exchange: Exchange name (default: "BIST")
Example:
stream.subscribe_chart("THYAO", "1m") stream.subscribe_chart("GARAN", "1h")
1567 def unsubscribe_chart( 1568 self, symbol: str, interval: str, exchange: str = "BIST" 1569 ) -> None: 1570 """ 1571 Unsubscribe from chart updates. 1572 1573 Args: 1574 symbol: Stock symbol 1575 interval: Candle interval 1576 exchange: Exchange name 1577 1578 Example: 1579 >>> stream.unsubscribe_chart("THYAO", "1m") 1580 """ 1581 symbol = symbol.upper() 1582 interval = interval.lower() 1583 1584 with self._lock: 1585 if symbol in self._chart_subscribed: 1586 self._chart_subscribed[symbol].discard(interval) 1587 if not self._chart_subscribed[symbol]: 1588 del self._chart_subscribed[symbol] 1589 1590 if symbol in self._chart_data and interval in self._chart_data[symbol]: 1591 del self._chart_data[symbol][interval] 1592 if not self._chart_data[symbol]: 1593 del self._chart_data[symbol] 1594 1595 event_key = f"{symbol}:{interval}" 1596 self._chart_events.pop(event_key, None) 1597 self._chart_callbacks.pop(event_key, None) 1598 1599 if self.is_connected: 1600 self._send_chart_unsubscribe(symbol, interval)
Unsubscribe from chart updates.
Args: symbol: Stock symbol interval: Candle interval exchange: Exchange name
Example:
stream.unsubscribe_chart("THYAO", "1m")
1602 def get_candle(self, symbol: str, interval: str) -> dict[str, Any] | None: 1603 """ 1604 Get latest cached candle (instant, ~1ms). 1605 1606 Args: 1607 symbol: Stock symbol 1608 interval: Candle interval 1609 1610 Returns: 1611 Candle dict or None if not subscribed/no data yet. 1612 Candle format: 1613 { 1614 "time": 1737123456, # Unix timestamp 1615 "open": 285.0, # Open price 1616 "high": 286.5, # High price 1617 "low": 284.0, # Low price 1618 "close": 285.5, # Close price 1619 "volume": 123456 # Volume 1620 } 1621 1622 Example: 1623 >>> candle = stream.get_candle("THYAO", "1m") 1624 >>> print(candle['close']) 1625 285.5 1626 """ 1627 symbol = symbol.upper() 1628 interval = interval.lower() 1629 1630 with self._lock: 1631 if symbol not in self._chart_data: 1632 return None 1633 if interval not in self._chart_data[symbol]: 1634 return None 1635 candles = self._chart_data[symbol][interval] 1636 if not candles: 1637 return None 1638 return candles[-1].copy()
Get latest cached candle (instant, ~1ms).
Args: symbol: Stock symbol interval: Candle interval
Returns: Candle dict or None if not subscribed/no data yet. Candle format: { "time": 1737123456, # Unix timestamp "open": 285.0, # Open price "high": 286.5, # High price "low": 284.0, # Low price "close": 285.5, # Close price "volume": 123456 # Volume }
Example:
candle = stream.get_candle("THYAO", "1m") print(candle['close']) 285.5
1640 def get_candles( 1641 self, symbol: str, interval: str, count: int | None = None 1642 ) -> list[dict[str, Any]]: 1643 """ 1644 Get cached candles. 1645 1646 Args: 1647 symbol: Stock symbol 1648 interval: Candle interval 1649 count: Number of candles to return (None = all) 1650 1651 Returns: 1652 List of candle dicts, oldest first. 1653 1654 Example: 1655 >>> candles = stream.get_candles("THYAO", "1m", count=10) 1656 >>> print(len(candles)) 1657 10 1658 """ 1659 symbol = symbol.upper() 1660 interval = interval.lower() 1661 1662 with self._lock: 1663 if symbol not in self._chart_data: 1664 return [] 1665 if interval not in self._chart_data[symbol]: 1666 return [] 1667 candles = self._chart_data[symbol][interval] 1668 if count: 1669 return [c.copy() for c in candles[-count:]] 1670 return [c.copy() for c in candles]
Get cached candles.
Args: symbol: Stock symbol interval: Candle interval count: Number of candles to return (None = all)
Returns: List of candle dicts, oldest first.
Example:
candles = stream.get_candles("THYAO", "1m", count=10) print(len(candles)) 10
1672 def wait_for_candle( 1673 self, symbol: str, interval: str, timeout: float = 5.0 1674 ) -> dict[str, Any]: 1675 """ 1676 Wait for first candle (blocking). 1677 1678 Useful after subscribing to ensure data is received. 1679 1680 Args: 1681 symbol: Stock symbol 1682 interval: Candle interval 1683 timeout: Maximum wait time in seconds 1684 1685 Returns: 1686 Candle dict 1687 1688 Raises: 1689 TimeoutError: If candle not received within timeout. 1690 1691 Example: 1692 >>> stream.subscribe_chart("THYAO", "1m") 1693 >>> candle = stream.wait_for_candle("THYAO", "1m") 1694 """ 1695 symbol = symbol.upper() 1696 interval = interval.lower() 1697 1698 # Check if already have data 1699 candle = self.get_candle(symbol, interval) 1700 if candle: 1701 return candle 1702 1703 # Wait for data 1704 event_key = f"{symbol}:{interval}" 1705 with self._lock: 1706 if event_key not in self._chart_events: 1707 self._chart_events[event_key] = threading.Event() 1708 event = self._chart_events[event_key] 1709 event.clear() 1710 1711 if not event.wait(timeout=timeout): 1712 raise TimeoutError(f"Timeout waiting for candle: {symbol} {interval}") 1713 1714 candle = self.get_candle(symbol, interval) 1715 if candle is None: 1716 raise TimeoutError(f"No candle data received for {symbol} {interval}") 1717 return candle
Wait for first candle (blocking).
Useful after subscribing to ensure data is received.
Args: symbol: Stock symbol interval: Candle interval timeout: Maximum wait time in seconds
Returns: Candle dict
Raises: TimeoutError: If candle not received within timeout.
Example:
stream.subscribe_chart("THYAO", "1m") candle = stream.wait_for_candle("THYAO", "1m")
1719 def on_candle( 1720 self, 1721 symbol: str, 1722 interval: str, 1723 callback: Callable[[str, str, dict], None], 1724 ) -> None: 1725 """ 1726 Register callback for candle updates. 1727 1728 Callback signature: callback(symbol: str, interval: str, candle: dict) 1729 1730 Args: 1731 symbol: Stock symbol 1732 interval: Candle interval 1733 callback: Function to call on each update 1734 1735 Example: 1736 >>> def on_candle_update(symbol, interval, candle): 1737 ... print(f"{symbol} {interval}: O={candle['open']} C={candle['close']}") 1738 >>> stream.on_candle("THYAO", "1m", on_candle_update) 1739 """ 1740 symbol = symbol.upper() 1741 interval = interval.lower() 1742 callback_key = f"{symbol}:{interval}" 1743 1744 with self._lock: 1745 if callback_key not in self._chart_callbacks: 1746 self._chart_callbacks[callback_key] = [] 1747 self._chart_callbacks[callback_key].append(callback)
Register callback for candle updates.
Callback signature: callback(symbol: str, interval: str, candle: dict)
Args: symbol: Stock symbol interval: Candle interval callback: Function to call on each update
Example:
def on_candle_update(symbol, interval, candle): ... print(f"{symbol} {interval}: O={candle['open']} C={candle['close']}") stream.on_candle("THYAO", "1m", on_candle_update)
1749 def on_any_candle( 1750 self, callback: Callable[[str, str, dict], None] 1751 ) -> None: 1752 """ 1753 Register callback for all candle updates. 1754 1755 Args: 1756 callback: Function to call on each update for any subscription. 1757 1758 Example: 1759 >>> def on_any_update(symbol, interval, candle): 1760 ... print(f"{symbol} {interval}: {candle['close']}") 1761 >>> stream.on_any_candle(on_any_update) 1762 """ 1763 self._global_chart_callbacks.append(callback)
Register callback for all candle updates.
Args: callback: Function to call on each update for any subscription.
Example:
def on_any_update(symbol, interval, candle): ... print(f"{symbol} {interval}: {candle['close']}") stream.on_any_candle(on_any_update)
1765 def remove_candle_callback( 1766 self, 1767 symbol: str, 1768 interval: str, 1769 callback: Callable[[str, str, dict], None], 1770 ) -> None: 1771 """ 1772 Remove a registered candle callback. 1773 1774 Args: 1775 symbol: Stock symbol 1776 interval: Candle interval 1777 callback: The callback to remove 1778 """ 1779 symbol = symbol.upper() 1780 interval = interval.lower() 1781 callback_key = f"{symbol}:{interval}" 1782 1783 with self._lock: 1784 if callback_key in self._chart_callbacks: 1785 try: 1786 self._chart_callbacks[callback_key].remove(callback) 1787 except ValueError: 1788 pass
Remove a registered candle callback.
Args: symbol: Stock symbol interval: Candle interval callback: The callback to remove
1790 @property 1791 def chart_subscriptions(self) -> dict[str, set[str]]: 1792 """Get current chart subscriptions. 1793 1794 Returns: 1795 Dict mapping symbol to set of subscribed intervals. 1796 """ 1797 with self._lock: 1798 return { 1799 sym: intervals.copy() 1800 for sym, intervals in self._chart_subscribed.items() 1801 }
Get current chart subscriptions.
Returns: Dict mapping symbol to set of subscribed intervals.
1803 def get_quote(self, symbol: str) -> dict[str, Any] | None: 1804 """ 1805 Get latest cached quote (instant, ~1ms). 1806 1807 Args: 1808 symbol: Stock symbol 1809 1810 Returns: 1811 Quote dict or None if not subscribed/no data yet. 1812 1813 Example: 1814 >>> quote = stream.get_quote("THYAO") 1815 >>> print(quote['last']) 1816 299.0 1817 """ 1818 symbol = symbol.upper() 1819 with self._lock: 1820 if symbol not in self._quotes: 1821 return None 1822 return self._build_quote(symbol)
Get latest cached quote (instant, ~1ms).
Args: symbol: Stock symbol
Returns: Quote dict or None if not subscribed/no data yet.
Example:
quote = stream.get_quote("THYAO") print(quote['last']) 299.0
1824 def wait_for_quote( 1825 self, symbol: str, timeout: float = 5.0 1826 ) -> dict[str, Any]: 1827 """ 1828 Wait for first quote (blocking). 1829 1830 Useful after subscribing to ensure data is received. 1831 1832 Args: 1833 symbol: Stock symbol 1834 timeout: Maximum wait time in seconds 1835 1836 Returns: 1837 Quote dict 1838 1839 Raises: 1840 TimeoutError: If quote not received within timeout. 1841 1842 Example: 1843 >>> stream.subscribe("THYAO") 1844 >>> quote = stream.wait_for_quote("THYAO") 1845 """ 1846 symbol = symbol.upper() 1847 1848 # Check if already have data 1849 quote = self.get_quote(symbol) 1850 if quote and quote.get("last") is not None: 1851 return quote 1852 1853 # Wait for data 1854 with self._lock: 1855 if symbol not in self._quote_events: 1856 self._quote_events[symbol] = threading.Event() 1857 event = self._quote_events[symbol] 1858 event.clear() 1859 1860 if not event.wait(timeout=timeout): 1861 raise TimeoutError(f"Timeout waiting for quote: {symbol}") 1862 1863 quote = self.get_quote(symbol) 1864 if quote is None: 1865 raise TimeoutError(f"No quote data received for {symbol}") 1866 return quote
Wait for first quote (blocking).
Useful after subscribing to ensure data is received.
Args: symbol: Stock symbol timeout: Maximum wait time in seconds
Returns: Quote dict
Raises: TimeoutError: If quote not received within timeout.
Example:
stream.subscribe("THYAO") quote = stream.wait_for_quote("THYAO")
1868 def on_quote( 1869 self, symbol: str, callback: Callable[[str, dict], None] 1870 ) -> None: 1871 """ 1872 Register callback for quote updates. 1873 1874 Callback signature: callback(symbol: str, quote: dict) 1875 1876 Args: 1877 symbol: Stock symbol 1878 callback: Function to call on each update 1879 1880 Example: 1881 >>> def on_price_update(symbol, quote): 1882 ... print(f"{symbol}: {quote['last']}") 1883 >>> stream.on_quote("THYAO", on_price_update) 1884 """ 1885 symbol = symbol.upper() 1886 with self._lock: 1887 if symbol not in self._callbacks: 1888 self._callbacks[symbol] = [] 1889 self._callbacks[symbol].append(callback)
Register callback for quote updates.
Callback signature: callback(symbol: str, quote: dict)
Args: symbol: Stock symbol callback: Function to call on each update
Example:
def on_price_update(symbol, quote): ... print(f"{symbol}: {quote['last']}") stream.on_quote("THYAO", on_price_update)
1891 def on_any_quote(self, callback: Callable[[str, dict], None]) -> None: 1892 """ 1893 Register callback for all quote updates. 1894 1895 Args: 1896 callback: Function to call on each update for any symbol. 1897 1898 Example: 1899 >>> def on_any_update(symbol, quote): 1900 ... print(f"{symbol}: {quote['last']}") 1901 >>> stream.on_any_quote(on_any_update) 1902 """ 1903 self._global_callbacks.append(callback)
Register callback for all quote updates.
Args: callback: Function to call on each update for any symbol.
Example:
def on_any_update(symbol, quote): ... print(f"{symbol}: {quote['last']}") stream.on_any_quote(on_any_update)
1905 def remove_callback( 1906 self, symbol: str, callback: Callable[[str, dict], None] 1907 ) -> None: 1908 """ 1909 Remove a registered callback. 1910 1911 Args: 1912 symbol: Stock symbol 1913 callback: The callback to remove 1914 """ 1915 symbol = symbol.upper() 1916 with self._lock: 1917 if symbol in self._callbacks: 1918 try: 1919 self._callbacks[symbol].remove(callback) 1920 except ValueError: 1921 pass
Remove a registered callback.
Args: symbol: Stock symbol callback: The callback to remove
1923 def wait(self) -> None: 1924 """ 1925 Block until disconnect. 1926 1927 Useful for keeping the stream alive in main thread. 1928 1929 Example: 1930 >>> stream.connect() 1931 >>> stream.subscribe("THYAO") 1932 >>> stream.on_quote("THYAO", my_callback) 1933 >>> stream.wait() # Blocks forever 1934 """ 1935 if self._ws_thread: 1936 self._ws_thread.join()
Block until disconnect.
Useful for keeping the stream alive in main thread.
Example:
stream.connect() stream.subscribe("THYAO") stream.on_quote("THYAO", my_callback) stream.wait() # Blocks forever
1951 @property 1952 def studies(self) -> StudySession: 1953 """ 1954 Access Pine Script study session. 1955 1956 Returns: 1957 StudySession instance for managing indicators. 1958 1959 Example: 1960 >>> stream.studies.add("THYAO", "1m", "RSI") 1961 >>> stream.studies.get("THYAO", "1m", "RSI") 1962 """ 1963 if self._study_session is None: 1964 self._study_session = StudySession(self) 1965 return self._study_session
Access Pine Script study session.
Returns: StudySession instance for managing indicators.
Example:
stream.studies.add("THYAO", "1m", "RSI") stream.studies.get("THYAO", "1m", "RSI")
1967 def add_study( 1968 self, 1969 symbol: str, 1970 interval: str, 1971 indicator: str, 1972 **kwargs: Any, 1973 ) -> str: 1974 """ 1975 Add a Pine indicator study (convenience method). 1976 1977 Args: 1978 symbol: Stock symbol (e.g., "THYAO") 1979 interval: Chart interval (e.g., "1m", "1d") 1980 indicator: Indicator name. Examples: 1981 - "RSI", "MACD", "BB" (standard) 1982 - "STD;RSI" (full standard ID) 1983 - "PUB;abc123" (community indicator) 1984 **kwargs: Indicator-specific parameters (e.g., length=14) 1985 1986 Returns: 1987 Study ID 1988 1989 Example: 1990 >>> stream.add_study("THYAO", "1m", "RSI") 1991 >>> stream.add_study("THYAO", "1m", "MACD") 1992 """ 1993 return self.studies.add(symbol, interval, indicator, **kwargs)
Add a Pine indicator study (convenience method).
Args: symbol: Stock symbol (e.g., "THYAO") interval: Chart interval (e.g., "1m", "1d") indicator: Indicator name. Examples: - "RSI", "MACD", "BB" (standard) - "STD;RSI" (full standard ID) - "PUB;abc123" (community indicator) **kwargs: Indicator-specific parameters (e.g., length=14)
Returns: Study ID
Example:
stream.add_study("THYAO", "1m", "RSI") stream.add_study("THYAO", "1m", "MACD")
1995 def remove_study(self, symbol: str, interval: str, indicator: str) -> None: 1996 """ 1997 Remove a Pine indicator study (convenience method). 1998 1999 Args: 2000 symbol: Stock symbol 2001 interval: Chart interval 2002 indicator: Indicator name 2003 2004 Example: 2005 >>> stream.remove_study("THYAO", "1m", "RSI") 2006 """ 2007 self.studies.remove(symbol, interval, indicator)
Remove a Pine indicator study (convenience method).
Args: symbol: Stock symbol interval: Chart interval indicator: Indicator name
Example:
stream.remove_study("THYAO", "1m", "RSI")
2009 def get_study( 2010 self, symbol: str, interval: str, indicator: str 2011 ) -> dict[str, Any] | None: 2012 """ 2013 Get latest study values (convenience method). 2014 2015 Args: 2016 symbol: Stock symbol 2017 interval: Chart interval 2018 indicator: Indicator name 2019 2020 Returns: 2021 Dict of indicator values or None. 2022 2023 Example: 2024 >>> rsi = stream.get_study("THYAO", "1m", "RSI") 2025 >>> print(rsi['value']) 2026 48.5 2027 """ 2028 return self.studies.get(symbol, interval, indicator)
Get latest study values (convenience method).
Args: symbol: Stock symbol interval: Chart interval indicator: Indicator name
Returns: Dict of indicator values or None.
Example:
rsi = stream.get_study("THYAO", "1m", "RSI") print(rsi['value']) 48.5
2030 def get_studies(self, symbol: str, interval: str) -> dict[str, dict[str, Any]]: 2031 """ 2032 Get all study values for a symbol/interval (convenience method). 2033 2034 Args: 2035 symbol: Stock symbol 2036 interval: Chart interval 2037 2038 Returns: 2039 Dict mapping indicator name to values. 2040 2041 Example: 2042 >>> studies = stream.get_studies("THYAO", "1m") 2043 >>> print(studies['RSI']['value']) 2044 48.5 2045 """ 2046 return self.studies.get_all(symbol, interval)
Get all study values for a symbol/interval (convenience method).
Args: symbol: Stock symbol interval: Chart interval
Returns: Dict mapping indicator name to values.
Example:
studies = stream.get_studies("THYAO", "1m") print(studies['RSI']['value']) 48.5
2048 def on_study( 2049 self, 2050 symbol: str, 2051 interval: str, 2052 indicator: str, 2053 callback: Callable[[str, str, str, dict], None], 2054 ) -> None: 2055 """ 2056 Register callback for study updates (convenience method). 2057 2058 Callback signature: callback(symbol, interval, indicator, values) 2059 2060 Args: 2061 symbol: Stock symbol 2062 interval: Chart interval 2063 indicator: Indicator name 2064 callback: Function to call on each update 2065 2066 Example: 2067 >>> def on_rsi(symbol, interval, indicator, values): 2068 ... print(f"{symbol} RSI: {values['value']}") 2069 >>> stream.on_study("THYAO", "1m", "RSI", on_rsi) 2070 """ 2071 self.studies.on_update(symbol, interval, indicator, callback)
Register callback for study updates (convenience method).
Callback signature: callback(symbol, interval, indicator, values)
Args: symbol: Stock symbol interval: Chart interval indicator: Indicator name callback: Function to call on each update
Example:
def on_rsi(symbol, interval, indicator, values): ... print(f"{symbol} RSI: {values['value']}") stream.on_study("THYAO", "1m", "RSI", on_rsi)
2073 def on_any_study( 2074 self, callback: Callable[[str, str, str, dict], None] 2075 ) -> None: 2076 """ 2077 Register callback for any study update (convenience method). 2078 2079 Args: 2080 callback: Function to call on each update. 2081 2082 Example: 2083 >>> stream.on_any_study( 2084 ... lambda s, i, n, v: print(f"{s} {n}: {v}") 2085 ... ) 2086 """ 2087 self.studies.on_any_update(callback)
Register callback for any study update (convenience method).
Args: callback: Function to call on each update.
Example:
stream.on_any_study( ... lambda s, i, n, v: print(f"{s} {n}: {v}") ... )
2089 def wait_for_study( 2090 self, symbol: str, interval: str, indicator: str, timeout: float = 10.0 2091 ) -> dict[str, Any]: 2092 """ 2093 Wait for study data (blocking convenience method). 2094 2095 Args: 2096 symbol: Stock symbol 2097 interval: Chart interval 2098 indicator: Indicator name 2099 timeout: Maximum wait time in seconds 2100 2101 Returns: 2102 Study values dict 2103 2104 Raises: 2105 TimeoutError: If data not received within timeout 2106 2107 Example: 2108 >>> stream.add_study("THYAO", "1m", "RSI") 2109 >>> rsi = stream.wait_for_study("THYAO", "1m", "RSI") 2110 """ 2111 return self.studies.wait_for(symbol, interval, indicator, timeout)
Wait for study data (blocking convenience method).
Args: symbol: Stock symbol interval: Chart interval indicator: Indicator name timeout: Maximum wait time in seconds
Returns: Study values dict
Raises: TimeoutError: If data not received within timeout
Example:
stream.add_study("THYAO", "1m", "RSI") rsi = stream.wait_for_study("THYAO", "1m", "RSI")
2115 def get_all_quotes(self) -> dict[str, dict[str, Any]]: 2116 """ 2117 Get all cached quotes. 2118 2119 Returns: 2120 Dict mapping symbol to quote dict. 2121 """ 2122 with self._lock: 2123 return {sym: self._build_quote(sym) for sym in self._quotes}
Get all cached quotes.
Returns: Dict mapping symbol to quote dict.
2125 def ping(self) -> float: 2126 """ 2127 Measure round-trip latency. 2128 2129 Returns: 2130 Latency in milliseconds. 2131 """ 2132 if not self.is_connected: 2133 return -1 2134 2135 start = time.time() 2136 # Send a heartbeat-like ping 2137 self._send(self._format_packet(f"~h~{self._heartbeat_counter}")) 2138 self._heartbeat_counter += 1 2139 elapsed = (time.time() - start) * 1000 2140 return elapsed
Measure round-trip latency.
Returns: Latency in milliseconds.
49class ReplaySession: 50 """ 51 Replay historical market data for backtesting. 52 53 Provides a generator-based interface for iterating over historical 54 OHLCV candles with speed control and callback support. 55 56 Attributes: 57 symbol: The stock symbol being replayed. 58 speed: Playback speed multiplier (1.0 = real-time). 59 total_candles: Total number of candles in the dataset. 60 61 Examples: 62 Basic usage:: 63 64 session = ReplaySession("THYAO", df=historical_data, speed=10.0) 65 for candle in session.replay(): 66 # Implement trading logic 67 pass 68 69 With callbacks:: 70 71 def my_callback(candle): 72 print(f"Candle {candle['_index']}: {candle['close']}") 73 74 session.on_candle(my_callback) 75 list(session.replay()) # Callbacks fire for each candle 76 """ 77 78 def __init__( 79 self, 80 symbol: str, 81 df: pd.DataFrame | None = None, 82 speed: float = 1.0, 83 realtime_injection: bool = False, 84 ): 85 """ 86 Initialize ReplaySession. 87 88 Args: 89 symbol: Stock symbol (e.g., "THYAO") 90 df: DataFrame with OHLCV data. Must have DatetimeIndex and 91 columns: Open, High, Low, Close, Volume. 92 If None, use create_replay() to load data automatically. 93 speed: Playback speed multiplier. 94 1.0 = real-time (60s between daily candles) 95 2.0 = 2x speed (30s between daily candles) 96 100.0 = fast forward (0.6s between daily candles) 97 0.0 or negative = no delay (as fast as possible) 98 realtime_injection: If True, candles are yielded at the 99 original time interval divided by speed. 100 If False, yields immediately with no delay. 101 102 Raises: 103 ValueError: If df is provided but missing required columns. 104 """ 105 self.symbol = symbol.upper() 106 self.speed = max(0.0, speed) 107 self.realtime_injection = realtime_injection 108 self._df: pd.DataFrame | None = df 109 self._callbacks: list[Callable[[dict], None]] = [] 110 self._current_index = 0 111 self._start_time: float | None = None 112 113 # Validate DataFrame if provided 114 if df is not None: 115 self._validate_dataframe(df) 116 117 def _validate_dataframe(self, df: pd.DataFrame) -> None: 118 """Validate that DataFrame has required columns.""" 119 required = {"Open", "High", "Low", "Close"} 120 missing = required - set(df.columns) 121 if missing: 122 raise ValueError(f"DataFrame missing required columns: {missing}") 123 124 @property 125 def total_candles(self) -> int: 126 """Get total number of candles.""" 127 if self._df is None: 128 return 0 129 return len(self._df) 130 131 @property 132 def progress(self) -> float: 133 """Get current replay progress (0.0 to 1.0).""" 134 if self.total_candles == 0: 135 return 0.0 136 return self._current_index / self.total_candles 137 138 def set_data(self, df: pd.DataFrame) -> None: 139 """ 140 Set the DataFrame for replay. 141 142 Args: 143 df: DataFrame with OHLCV data. 144 """ 145 self._validate_dataframe(df) 146 self._df = df 147 self._current_index = 0 148 149 def on_candle(self, callback: Callable[[dict], None]) -> None: 150 """ 151 Register callback for candle updates. 152 153 Callback signature: callback(candle: dict) 154 155 Args: 156 callback: Function to call for each candle during replay. 157 158 Example: 159 >>> def my_handler(candle): 160 ... print(f"Price: {candle['close']}") 161 >>> session.on_candle(my_handler) 162 """ 163 self._callbacks.append(callback) 164 165 def remove_callback(self, callback: Callable[[dict], None]) -> None: 166 """ 167 Remove a registered callback. 168 169 Args: 170 callback: The callback to remove. 171 """ 172 try: 173 self._callbacks.remove(callback) 174 except ValueError: 175 pass 176 177 def _build_candle(self, idx: int) -> dict[str, Any]: 178 """Build candle dict from DataFrame row.""" 179 if self._df is None: 180 return {} 181 182 row = self._df.iloc[idx] 183 timestamp = self._df.index[idx] 184 185 # Convert timestamp to datetime if needed 186 if isinstance(timestamp, pd.Timestamp): 187 timestamp = timestamp.to_pydatetime() 188 189 candle = { 190 "timestamp": timestamp, 191 "open": float(row["Open"]), 192 "high": float(row["High"]), 193 "low": float(row["Low"]), 194 "close": float(row["Close"]), 195 "volume": float(row.get("Volume", 0)) if "Volume" in row else 0, 196 "_index": idx, 197 "_total": len(self._df), 198 "_progress": (idx + 1) / len(self._df), 199 } 200 201 return candle 202 203 def _calculate_delay(self, current_idx: int) -> float: 204 """Calculate delay between candles based on speed.""" 205 if not self.realtime_injection or self.speed <= 0: 206 return 0.0 207 208 if self._df is None or current_idx == 0: 209 return 0.0 210 211 # Calculate time difference between candles 212 current_time = self._df.index[current_idx] 213 prev_time = self._df.index[current_idx - 1] 214 215 if isinstance(current_time, pd.Timestamp): 216 current_time = current_time.to_pydatetime() 217 if isinstance(prev_time, pd.Timestamp): 218 prev_time = prev_time.to_pydatetime() 219 220 time_diff = (current_time - prev_time).total_seconds() 221 222 # Apply speed multiplier 223 return time_diff / self.speed 224 225 def replay(self) -> Generator[dict[str, Any], None, None]: 226 """ 227 Generator that yields candles one by one. 228 229 Yields candles from the dataset with optional time delay 230 based on speed setting. 231 232 Yields: 233 Candle dict with keys: 234 - timestamp: datetime of the candle 235 - open: Open price 236 - high: High price 237 - low: Low price 238 - close: Close price 239 - volume: Trading volume 240 - _index: Current candle index (0-based) 241 - _total: Total number of candles 242 - _progress: Progress ratio (0.0 to 1.0) 243 244 Example: 245 >>> for candle in session.replay(): 246 ... print(f"{candle['timestamp']}: {candle['close']}") 247 """ 248 if self._df is None or len(self._df) == 0: 249 return 250 251 self._current_index = 0 252 self._start_time = time.time() 253 254 for idx in range(len(self._df)): 255 self._current_index = idx 256 257 # Calculate and apply delay 258 if idx > 0: 259 delay = self._calculate_delay(idx) 260 if delay > 0: 261 time.sleep(delay) 262 263 # Build candle 264 candle = self._build_candle(idx) 265 266 # Fire callbacks 267 for callback in self._callbacks: 268 try: 269 callback(candle) 270 except Exception: 271 pass # Silently ignore callback errors 272 273 yield candle 274 275 def replay_filtered( 276 self, 277 start_date: str | datetime | None = None, 278 end_date: str | datetime | None = None, 279 ) -> Generator[dict[str, Any], None, None]: 280 """ 281 Generator that yields filtered candles. 282 283 Filter candles by date range before replay. 284 285 Args: 286 start_date: Start date (inclusive). Can be string "YYYY-MM-DD" 287 or datetime object. 288 end_date: End date (inclusive). Can be string "YYYY-MM-DD" 289 or datetime object. 290 291 Yields: 292 Filtered candle dicts (same format as replay()). 293 294 Example: 295 >>> for candle in session.replay_filtered( 296 ... start_date="2024-01-01", 297 ... end_date="2024-06-30" 298 ... ): 299 ... # Process candle in date range 300 ... pass 301 """ 302 if self._df is None or len(self._df) == 0: 303 return 304 305 # Parse dates 306 if isinstance(start_date, str): 307 start_date = pd.to_datetime(start_date) 308 if isinstance(end_date, str): 309 end_date = pd.to_datetime(end_date) 310 311 # Handle timezone compatibility 312 # If DataFrame index is timezone-aware, convert filter dates to match 313 if self._df.index.tz is not None: 314 if start_date is not None and start_date.tzinfo is None: 315 start_date = start_date.tz_localize(self._df.index.tz) 316 if end_date is not None and end_date.tzinfo is None: 317 end_date = end_date.tz_localize(self._df.index.tz) 318 319 # Filter DataFrame using .date() comparison for timezone safety 320 mask = pd.Series([True] * len(self._df), index=self._df.index) 321 322 if start_date is not None: 323 mask &= self._df.index >= start_date 324 325 if end_date is not None: 326 mask &= self._df.index <= end_date 327 328 filtered_indices = self._df.index[mask] 329 330 self._current_index = 0 331 self._start_time = time.time() 332 333 for i, idx in enumerate(filtered_indices): 334 self._current_index = i 335 336 # Get the position in original DataFrame 337 original_idx = self._df.index.get_loc(idx) 338 339 # Calculate and apply delay 340 if i > 0: 341 delay = self._calculate_delay(original_idx) 342 if delay > 0: 343 time.sleep(delay) 344 345 # Build candle 346 candle = self._build_candle(original_idx) 347 348 # Update filtered progress 349 candle["_index"] = i 350 candle["_total"] = len(filtered_indices) 351 candle["_progress"] = (i + 1) / len(filtered_indices) 352 353 # Fire callbacks 354 for callback in self._callbacks: 355 try: 356 callback(candle) 357 except Exception: 358 pass 359 360 yield candle 361 362 def stats(self) -> dict[str, Any]: 363 """ 364 Get replay statistics. 365 366 Returns: 367 Dict with statistics: 368 - symbol: Stock symbol 369 - total_candles: Total number of candles 370 - current_index: Current position in replay 371 - progress: Progress ratio (0.0 to 1.0) 372 - speed: Playback speed multiplier 373 - elapsed_time: Time elapsed since replay start (seconds) 374 - start_date: First candle date 375 - end_date: Last candle date 376 377 Example: 378 >>> print(session.stats()) 379 {'symbol': 'THYAO', 'total_candles': 252, ...} 380 """ 381 stats = { 382 "symbol": self.symbol, 383 "total_candles": self.total_candles, 384 "current_index": self._current_index, 385 "progress": self.progress, 386 "speed": self.speed, 387 "realtime_injection": self.realtime_injection, 388 "elapsed_time": ( 389 time.time() - self._start_time if self._start_time else 0.0 390 ), 391 "start_date": None, 392 "end_date": None, 393 "callbacks_registered": len(self._callbacks), 394 } 395 396 if self._df is not None and len(self._df) > 0: 397 stats["start_date"] = self._df.index[0] 398 stats["end_date"] = self._df.index[-1] 399 400 if isinstance(stats["start_date"], pd.Timestamp): 401 stats["start_date"] = stats["start_date"].to_pydatetime() 402 if isinstance(stats["end_date"], pd.Timestamp): 403 stats["end_date"] = stats["end_date"].to_pydatetime() 404 405 return stats 406 407 def reset(self) -> None: 408 """Reset replay to beginning.""" 409 self._current_index = 0 410 self._start_time = None
Replay historical market data for backtesting.
Provides a generator-based interface for iterating over historical OHLCV candles with speed control and callback support.
Attributes: symbol: The stock symbol being replayed. speed: Playback speed multiplier (1.0 = real-time). total_candles: Total number of candles in the dataset.
Examples: Basic usage::
session = ReplaySession("THYAO", df=historical_data, speed=10.0)
for candle in session.replay():
# Implement trading logic
pass
With callbacks::
def my_callback(candle):
print(f"Candle {candle['_index']}: {candle['close']}")
session.on_candle(my_callback)
list(session.replay()) # Callbacks fire for each candle
78 def __init__( 79 self, 80 symbol: str, 81 df: pd.DataFrame | None = None, 82 speed: float = 1.0, 83 realtime_injection: bool = False, 84 ): 85 """ 86 Initialize ReplaySession. 87 88 Args: 89 symbol: Stock symbol (e.g., "THYAO") 90 df: DataFrame with OHLCV data. Must have DatetimeIndex and 91 columns: Open, High, Low, Close, Volume. 92 If None, use create_replay() to load data automatically. 93 speed: Playback speed multiplier. 94 1.0 = real-time (60s between daily candles) 95 2.0 = 2x speed (30s between daily candles) 96 100.0 = fast forward (0.6s between daily candles) 97 0.0 or negative = no delay (as fast as possible) 98 realtime_injection: If True, candles are yielded at the 99 original time interval divided by speed. 100 If False, yields immediately with no delay. 101 102 Raises: 103 ValueError: If df is provided but missing required columns. 104 """ 105 self.symbol = symbol.upper() 106 self.speed = max(0.0, speed) 107 self.realtime_injection = realtime_injection 108 self._df: pd.DataFrame | None = df 109 self._callbacks: list[Callable[[dict], None]] = [] 110 self._current_index = 0 111 self._start_time: float | None = None 112 113 # Validate DataFrame if provided 114 if df is not None: 115 self._validate_dataframe(df)
Initialize ReplaySession.
Args: symbol: Stock symbol (e.g., "THYAO") df: DataFrame with OHLCV data. Must have DatetimeIndex and columns: Open, High, Low, Close, Volume. If None, use create_replay() to load data automatically. speed: Playback speed multiplier. 1.0 = real-time (60s between daily candles) 2.0 = 2x speed (30s between daily candles) 100.0 = fast forward (0.6s between daily candles) 0.0 or negative = no delay (as fast as possible) realtime_injection: If True, candles are yielded at the original time interval divided by speed. If False, yields immediately with no delay.
Raises: ValueError: If df is provided but missing required columns.
124 @property 125 def total_candles(self) -> int: 126 """Get total number of candles.""" 127 if self._df is None: 128 return 0 129 return len(self._df)
Get total number of candles.
131 @property 132 def progress(self) -> float: 133 """Get current replay progress (0.0 to 1.0).""" 134 if self.total_candles == 0: 135 return 0.0 136 return self._current_index / self.total_candles
Get current replay progress (0.0 to 1.0).
138 def set_data(self, df: pd.DataFrame) -> None: 139 """ 140 Set the DataFrame for replay. 141 142 Args: 143 df: DataFrame with OHLCV data. 144 """ 145 self._validate_dataframe(df) 146 self._df = df 147 self._current_index = 0
Set the DataFrame for replay.
Args: df: DataFrame with OHLCV data.
149 def on_candle(self, callback: Callable[[dict], None]) -> None: 150 """ 151 Register callback for candle updates. 152 153 Callback signature: callback(candle: dict) 154 155 Args: 156 callback: Function to call for each candle during replay. 157 158 Example: 159 >>> def my_handler(candle): 160 ... print(f"Price: {candle['close']}") 161 >>> session.on_candle(my_handler) 162 """ 163 self._callbacks.append(callback)
Register callback for candle updates.
Callback signature: callback(candle: dict)
Args: callback: Function to call for each candle during replay.
Example:
def my_handler(candle): ... print(f"Price: {candle['close']}") session.on_candle(my_handler)
165 def remove_callback(self, callback: Callable[[dict], None]) -> None: 166 """ 167 Remove a registered callback. 168 169 Args: 170 callback: The callback to remove. 171 """ 172 try: 173 self._callbacks.remove(callback) 174 except ValueError: 175 pass
Remove a registered callback.
Args: callback: The callback to remove.
225 def replay(self) -> Generator[dict[str, Any], None, None]: 226 """ 227 Generator that yields candles one by one. 228 229 Yields candles from the dataset with optional time delay 230 based on speed setting. 231 232 Yields: 233 Candle dict with keys: 234 - timestamp: datetime of the candle 235 - open: Open price 236 - high: High price 237 - low: Low price 238 - close: Close price 239 - volume: Trading volume 240 - _index: Current candle index (0-based) 241 - _total: Total number of candles 242 - _progress: Progress ratio (0.0 to 1.0) 243 244 Example: 245 >>> for candle in session.replay(): 246 ... print(f"{candle['timestamp']}: {candle['close']}") 247 """ 248 if self._df is None or len(self._df) == 0: 249 return 250 251 self._current_index = 0 252 self._start_time = time.time() 253 254 for idx in range(len(self._df)): 255 self._current_index = idx 256 257 # Calculate and apply delay 258 if idx > 0: 259 delay = self._calculate_delay(idx) 260 if delay > 0: 261 time.sleep(delay) 262 263 # Build candle 264 candle = self._build_candle(idx) 265 266 # Fire callbacks 267 for callback in self._callbacks: 268 try: 269 callback(candle) 270 except Exception: 271 pass # Silently ignore callback errors 272 273 yield candle
Generator that yields candles one by one.
Yields candles from the dataset with optional time delay based on speed setting.
Yields: Candle dict with keys: - timestamp: datetime of the candle - open: Open price - high: High price - low: Low price - close: Close price - volume: Trading volume - _index: Current candle index (0-based) - _total: Total number of candles - _progress: Progress ratio (0.0 to 1.0)
Example:
for candle in session.replay(): ... print(f"{candle['timestamp']}: {candle['close']}")
275 def replay_filtered( 276 self, 277 start_date: str | datetime | None = None, 278 end_date: str | datetime | None = None, 279 ) -> Generator[dict[str, Any], None, None]: 280 """ 281 Generator that yields filtered candles. 282 283 Filter candles by date range before replay. 284 285 Args: 286 start_date: Start date (inclusive). Can be string "YYYY-MM-DD" 287 or datetime object. 288 end_date: End date (inclusive). Can be string "YYYY-MM-DD" 289 or datetime object. 290 291 Yields: 292 Filtered candle dicts (same format as replay()). 293 294 Example: 295 >>> for candle in session.replay_filtered( 296 ... start_date="2024-01-01", 297 ... end_date="2024-06-30" 298 ... ): 299 ... # Process candle in date range 300 ... pass 301 """ 302 if self._df is None or len(self._df) == 0: 303 return 304 305 # Parse dates 306 if isinstance(start_date, str): 307 start_date = pd.to_datetime(start_date) 308 if isinstance(end_date, str): 309 end_date = pd.to_datetime(end_date) 310 311 # Handle timezone compatibility 312 # If DataFrame index is timezone-aware, convert filter dates to match 313 if self._df.index.tz is not None: 314 if start_date is not None and start_date.tzinfo is None: 315 start_date = start_date.tz_localize(self._df.index.tz) 316 if end_date is not None and end_date.tzinfo is None: 317 end_date = end_date.tz_localize(self._df.index.tz) 318 319 # Filter DataFrame using .date() comparison for timezone safety 320 mask = pd.Series([True] * len(self._df), index=self._df.index) 321 322 if start_date is not None: 323 mask &= self._df.index >= start_date 324 325 if end_date is not None: 326 mask &= self._df.index <= end_date 327 328 filtered_indices = self._df.index[mask] 329 330 self._current_index = 0 331 self._start_time = time.time() 332 333 for i, idx in enumerate(filtered_indices): 334 self._current_index = i 335 336 # Get the position in original DataFrame 337 original_idx = self._df.index.get_loc(idx) 338 339 # Calculate and apply delay 340 if i > 0: 341 delay = self._calculate_delay(original_idx) 342 if delay > 0: 343 time.sleep(delay) 344 345 # Build candle 346 candle = self._build_candle(original_idx) 347 348 # Update filtered progress 349 candle["_index"] = i 350 candle["_total"] = len(filtered_indices) 351 candle["_progress"] = (i + 1) / len(filtered_indices) 352 353 # Fire callbacks 354 for callback in self._callbacks: 355 try: 356 callback(candle) 357 except Exception: 358 pass 359 360 yield candle
Generator that yields filtered candles.
Filter candles by date range before replay.
Args: start_date: Start date (inclusive). Can be string "YYYY-MM-DD" or datetime object. end_date: End date (inclusive). Can be string "YYYY-MM-DD" or datetime object.
Yields: Filtered candle dicts (same format as replay()).
Example:
for candle in session.replay_filtered( ... start_date="2024-01-01", ... end_date="2024-06-30" ... ): ... # Process candle in date range ... pass
362 def stats(self) -> dict[str, Any]: 363 """ 364 Get replay statistics. 365 366 Returns: 367 Dict with statistics: 368 - symbol: Stock symbol 369 - total_candles: Total number of candles 370 - current_index: Current position in replay 371 - progress: Progress ratio (0.0 to 1.0) 372 - speed: Playback speed multiplier 373 - elapsed_time: Time elapsed since replay start (seconds) 374 - start_date: First candle date 375 - end_date: Last candle date 376 377 Example: 378 >>> print(session.stats()) 379 {'symbol': 'THYAO', 'total_candles': 252, ...} 380 """ 381 stats = { 382 "symbol": self.symbol, 383 "total_candles": self.total_candles, 384 "current_index": self._current_index, 385 "progress": self.progress, 386 "speed": self.speed, 387 "realtime_injection": self.realtime_injection, 388 "elapsed_time": ( 389 time.time() - self._start_time if self._start_time else 0.0 390 ), 391 "start_date": None, 392 "end_date": None, 393 "callbacks_registered": len(self._callbacks), 394 } 395 396 if self._df is not None and len(self._df) > 0: 397 stats["start_date"] = self._df.index[0] 398 stats["end_date"] = self._df.index[-1] 399 400 if isinstance(stats["start_date"], pd.Timestamp): 401 stats["start_date"] = stats["start_date"].to_pydatetime() 402 if isinstance(stats["end_date"], pd.Timestamp): 403 stats["end_date"] = stats["end_date"].to_pydatetime() 404 405 return stats
Get replay statistics.
Returns: Dict with statistics: - symbol: Stock symbol - total_candles: Total number of candles - current_index: Current position in replay - progress: Progress ratio (0.0 to 1.0) - speed: Playback speed multiplier - elapsed_time: Time elapsed since replay start (seconds) - start_date: First candle date - end_date: Last candle date
Example:
print(session.stats()) {'symbol': 'THYAO', 'total_candles': 252, ...}
9def companies() -> pd.DataFrame: 10 """ 11 Get list of all BIST companies. 12 13 Returns: 14 DataFrame with columns: 15 - ticker: Stock ticker code (e.g., "THYAO", "GARAN") 16 - name: Company name 17 - city: Company headquarters city 18 19 Examples: 20 >>> import borsapy as bp 21 >>> bp.companies() 22 ticker name city 23 0 ACSEL ACIPAYAM SELULOZ SANAYI A.S. DENIZLI 24 1 ADEL ADEL KALEMCILIK A.S. ISTANBUL 25 ... 26 """ 27 provider = get_kap_provider() 28 return provider.get_companies()
Get list of all BIST companies.
Returns: DataFrame with columns: - ticker: Stock ticker code (e.g., "THYAO", "GARAN") - name: Company name - city: Company headquarters city
Examples:
import borsapy as bp bp.companies() ticker name city 0 ACSEL ACIPAYAM SELULOZ SANAYI A.S. DENIZLI 1 ADEL ADEL KALEMCILIK A.S. ISTANBUL ...
31def search_companies(query: str) -> pd.DataFrame: 32 """ 33 Search BIST companies by name or ticker. 34 35 Args: 36 query: Search query (ticker code or company name) 37 38 Returns: 39 DataFrame with matching companies, sorted by relevance. 40 41 Examples: 42 >>> import borsapy as bp 43 >>> bp.search_companies("THYAO") 44 ticker name city 45 0 THYAO TURK HAVA YOLLARI A.O. ISTANBUL 46 47 >>> bp.search_companies("banka") 48 ticker name city 49 0 GARAN TURKIYE GARANTI BANKASI A.S. ISTANBUL 50 1 AKBNK AKBANK T.A.S. ISTANBUL 51 ... 52 """ 53 provider = get_kap_provider() 54 return provider.search(query)
Search BIST companies by name or ticker.
Args: query: Search query (ticker code or company name)
Returns: DataFrame with matching companies, sorted by relevance.
Examples:
import borsapy as bp bp.search_companies("THYAO") ticker name city 0 THYAO TURK HAVA YOLLARI A.O. ISTANBUL
>>> bp.search_companies("banka") ticker name city 0 GARAN TURKIYE GARANTI BANKASI A.S. ISTANBUL 1 AKBNK AKBANK T.A.S. ISTANBUL ...
26def search( 27 query: str, 28 type: str | None = None, 29 exchange: str | None = None, 30 limit: int = 50, 31 full_info: bool = False, 32) -> list[str] | list[dict[str, Any]]: 33 """Search for symbols matching the query. 34 35 Searches TradingView's symbol database and optionally merges with local 36 KAP company data for comprehensive BIST coverage. 37 38 Args: 39 query: Search query (e.g., "banka", "enerji", "THY", "gold") 40 type: Filter by asset type: 41 - "stock": Stocks/equities 42 - "forex" or "fx": Forex pairs 43 - "crypto": Cryptocurrencies 44 - "index": Market indices 45 - "futures": Futures contracts 46 - "fund" or "etf": Funds and ETFs 47 - "bond": Bonds 48 exchange: Filter by exchange (e.g., "BIST", "NASDAQ", "NYSE") 49 limit: Maximum number of results (default 50, max 100) 50 full_info: If True, return full result dicts; if False, return symbol list 51 52 Returns: 53 If full_info=False (default): 54 List of symbol strings: ['AKBNK', 'GARAN', ...] 55 56 If full_info=True: 57 List of result dicts: 58 [ 59 { 60 "symbol": "AKBNK", 61 "full_name": "BIST:AKBNK", 62 "description": "AKBANK T.A.S.", 63 "exchange": "BIST", 64 "type": "stock", 65 "currency": "TRY", 66 "country": "TR" 67 }, 68 ... 69 ] 70 71 Examples: 72 >>> import borsapy as bp 73 74 >>> # Simple search 75 >>> bp.search("banka") 76 ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB', ...] 77 78 >>> # Search with type filter 79 >>> bp.search("gold", type="forex") 80 ['XAUUSD', 'XAUTRY', ...] 81 82 >>> # Search with exchange filter 83 >>> bp.search("THY", exchange="BIST") 84 ['THYAO'] 85 86 >>> # Get full info 87 >>> bp.search("GARAN", full_info=True) 88 [{'symbol': 'GARAN', 'description': 'TURKIYE GARANTI BANKASI A.S.', ...}] 89 90 >>> # Search crypto 91 >>> bp.search("BTC", type="crypto") 92 ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...] 93 94 >>> # Search indices 95 >>> bp.search("XU", type="index", exchange="BIST") 96 ['XU100', 'XU030', 'XU050', ...] 97 98 Raises: 99 ValueError: If query is empty 100 """ 101 if not query or not query.strip(): 102 raise ValueError("Search query cannot be empty") 103 104 from borsapy._providers.tradingview_search import get_search_provider 105 106 provider = get_search_provider() 107 108 # Search TradingView 109 results = provider.search( 110 query=query, 111 asset_type=type, 112 exchange=exchange, 113 limit=limit, 114 ) 115 116 # Try to enhance with KAP data for BIST stocks 117 if not type or type.lower() == "stock": 118 if not exchange or exchange.upper() == "BIST": 119 results = _merge_with_kap(results, query) 120 121 if full_info: 122 return results 123 else: 124 # Return just symbol list (deduplicated, maintaining order) 125 seen = set() 126 symbols = [] 127 for r in results: 128 sym = r.get("symbol", "") 129 if sym and sym not in seen: 130 seen.add(sym) 131 symbols.append(sym) 132 return symbols
Search for symbols matching the query.
Searches TradingView's symbol database and optionally merges with local KAP company data for comprehensive BIST coverage.
Args: query: Search query (e.g., "banka", "enerji", "THY", "gold") type: Filter by asset type: - "stock": Stocks/equities - "forex" or "fx": Forex pairs - "crypto": Cryptocurrencies - "index": Market indices - "futures": Futures contracts - "fund" or "etf": Funds and ETFs - "bond": Bonds exchange: Filter by exchange (e.g., "BIST", "NASDAQ", "NYSE") limit: Maximum number of results (default 50, max 100) full_info: If True, return full result dicts; if False, return symbol list
Returns: If full_info=False (default): List of symbol strings: ['AKBNK', 'GARAN', ...]
If full_info=True:
List of result dicts:
[
{
"symbol": "AKBNK",
"full_name": "BIST:AKBNK",
"description": "AKBANK T.A.S.",
"exchange": "BIST",
"type": "stock",
"currency": "TRY",
"country": "TR"
},
...
]
Examples:
import borsapy as bp
>>> # Simple search >>> bp.search("banka") ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB', ...] >>> # Search with type filter >>> bp.search("gold", type="forex") ['XAUUSD', 'XAUTRY', ...] >>> # Search with exchange filter >>> bp.search("THY", exchange="BIST") ['THYAO'] >>> # Get full info >>> bp.search("GARAN", full_info=True) [{'symbol': 'GARAN', 'description': 'TURKIYE GARANTI BANKASI A.S.', ...}] >>> # Search crypto >>> bp.search("BTC", type="crypto") ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...] >>> # Search indices >>> bp.search("XU", type="index", exchange="BIST") ['XU100', 'XU030', 'XU050', ...]Raises: ValueError: If query is empty
180def search_bist(query: str, limit: int = 50) -> list[str]: 181 """Search BIST symbols only. 182 183 Convenience function for Turkish stock search. 184 185 Args: 186 query: Search query 187 limit: Maximum results 188 189 Returns: 190 List of BIST symbol strings 191 192 Examples: 193 >>> bp.search_bist("banka") 194 ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB'] 195 """ 196 return search(query, type="stock", exchange="BIST", limit=limit)
Search BIST symbols only.
Convenience function for Turkish stock search.
Args: query: Search query limit: Maximum results
Returns: List of BIST symbol strings
Examples:
bp.search_bist("banka") ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB']
199def search_crypto(query: str, limit: int = 50) -> list[str]: 200 """Search cryptocurrency symbols. 201 202 Args: 203 query: Search query (e.g., "BTC", "ETH") 204 limit: Maximum results 205 206 Returns: 207 List of crypto symbol strings 208 209 Examples: 210 >>> bp.search_crypto("BTC") 211 ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...] 212 """ 213 return search(query, type="crypto", limit=limit)
Search cryptocurrency symbols.
Args: query: Search query (e.g., "BTC", "ETH") limit: Maximum results
Returns: List of crypto symbol strings
Examples:
bp.search_crypto("BTC") ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...]
216def search_forex(query: str, limit: int = 50) -> list[str]: 217 """Search forex symbols. 218 219 Args: 220 query: Search query (e.g., "USD", "EUR", "gold") 221 limit: Maximum results 222 223 Returns: 224 List of forex symbol strings 225 226 Examples: 227 >>> bp.search_forex("gold") 228 ['XAUUSD', 'XAUTRY', ...] 229 """ 230 return search(query, type="forex", limit=limit)
Search forex symbols.
Args: query: Search query (e.g., "USD", "EUR", "gold") limit: Maximum results
Returns: List of forex symbol strings
Examples:
bp.search_forex("gold") ['XAUUSD', 'XAUTRY', ...]
233def search_index(query: str, limit: int = 50) -> list[str]: 234 """Search market index symbols. 235 236 Args: 237 query: Search query (e.g., "XU", "SP500") 238 limit: Maximum results 239 240 Returns: 241 List of index symbol strings 242 243 Examples: 244 >>> bp.search_index("XU") 245 ['XU100', 'XU030', 'XU050', ...] 246 """ 247 return search(query, type="index", limit=limit)
Search market index symbols.
Args: query: Search query (e.g., "XU", "SP500") limit: Maximum results
Returns: List of index symbol strings
Examples:
bp.search_index("XU") ['XU100', 'XU030', 'XU050', ...]
250def search_viop(query: str, limit: int = 50) -> list[str]: 251 """Search VIOP (Turkish derivatives) symbols. 252 253 Searches for futures and options contracts on BIST. 254 255 Args: 256 query: Search query (e.g., "XU030", "AKBNK", "gold") 257 limit: Maximum results 258 259 Returns: 260 List of VIOP contract symbol strings 261 262 Examples: 263 >>> bp.search_viop("XU030") 264 ['XU030D', 'XU030DG2026', 'XU030DJ2026', ...] 265 266 >>> bp.search_viop("gold") 267 ['XAUTRYD', 'XAUTRYG2026', ...] 268 """ 269 return search(query, type="futures", exchange="BIST", limit=limit)
Search VIOP (Turkish derivatives) symbols.
Searches for futures and options contracts on BIST.
Args: query: Search query (e.g., "XU030", "AKBNK", "gold") limit: Maximum results
Returns: List of VIOP contract symbol strings
Examples:
bp.search_viop("XU030") ['XU030D', 'XU030DG2026', 'XU030DJ2026', ...]
>>> bp.search_viop("gold") ['XAUTRYD', 'XAUTRYG2026', ...]
272def viop_contracts( 273 base_symbol: str, 274 full_info: bool = False, 275) -> list[str] | list[dict]: 276 """Get available VIOP contracts for a base symbol. 277 278 Queries TradingView to find all active contracts (expiry months) 279 for a given futures base symbol. 280 281 Args: 282 base_symbol: Base futures symbol (e.g., "XU030D", "XAUTRYD", "USDTRYD") 283 Can be with or without 'D' suffix. 284 full_info: If True, return full contract info dicts 285 286 Returns: 287 If full_info=False (default): 288 List of contract symbol strings: ['XU030DG2026', 'XU030DJ2026'] 289 290 If full_info=True: 291 List of contract dicts with month/year info 292 293 Note: 294 Contract month codes: 295 F=Jan, G=Feb, H=Mar, J=Apr, K=May, M=Jun, 296 N=Jul, Q=Aug, U=Sep, V=Oct, X=Nov, Z=Dec 297 298 Examples: 299 >>> import borsapy as bp 300 301 >>> # Get BIST30 futures contracts 302 >>> bp.viop_contracts("XU030D") 303 ['XU030DG2026', 'XU030DJ2026'] 304 305 >>> # Get gold TRY futures 306 >>> bp.viop_contracts("XAUTRYD") 307 ['XAUTRYG2026', 'XAUTRYJ2026'] 308 309 >>> # Get full contract info 310 >>> bp.viop_contracts("XU030D", full_info=True) 311 [ 312 {'symbol': 'XU030DG2026', 'month_code': 'G', 'year': '2026', ...}, 313 {'symbol': 'XU030DJ2026', 'month_code': 'J', 'year': '2026', ...}, 314 ] 315 316 See Also: 317 search_viop: Search for VIOP symbols by keyword 318 """ 319 from borsapy._providers.tradingview_search import get_search_provider 320 321 provider = get_search_provider() 322 contracts = provider.get_viop_contracts(base_symbol) 323 324 if full_info: 325 return contracts 326 else: 327 # Filter out continuous contracts (they don't work with streaming) 328 return [c["symbol"] for c in contracts if not c.get("is_continuous", False)]
Get available VIOP contracts for a base symbol.
Queries TradingView to find all active contracts (expiry months) for a given futures base symbol.
Args: base_symbol: Base futures symbol (e.g., "XU030D", "XAUTRYD", "USDTRYD") Can be with or without 'D' suffix. full_info: If True, return full contract info dicts
Returns: If full_info=False (default): List of contract symbol strings: ['XU030DG2026', 'XU030DJ2026']
If full_info=True:
List of contract dicts with month/year info
Note: Contract month codes: F=Jan, G=Feb, H=Mar, J=Apr, K=May, M=Jun, N=Jul, Q=Aug, U=Sep, V=Oct, X=Nov, Z=Dec
Examples:
import borsapy as bp
>>> # Get BIST30 futures contracts >>> bp.viop_contracts("XU030D") ['XU030DG2026', 'XU030DJ2026'] >>> # Get gold TRY futures >>> bp.viop_contracts("XAUTRYD") ['XAUTRYG2026', 'XAUTRYJ2026'] >>> # Get full contract info >>> bp.viop_contracts("XU030D", full_info=True) [ {'symbol': 'XU030DG2026', 'month_code': 'G', 'year': '2026', ...}, {'symbol': 'XU030DJ2026', 'month_code': 'J', 'year': '2026', ...}, ]See Also: search_viop: Search for VIOP symbols by keyword
43def banks() -> list[str]: 44 """ 45 Get list of supported banks for exchange rates. 46 47 Returns: 48 List of bank codes. 49 50 Examples: 51 >>> import borsapy as bp 52 >>> bp.banks() 53 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 54 """ 55 return get_dovizcom_provider().get_banks()
Get list of supported banks for exchange rates.
Returns: List of bank codes.
Examples:
import borsapy as bp bp.banks() ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
58def metal_institutions() -> list[str]: 59 """ 60 Get list of supported precious metal assets for institution rates. 61 62 Returns: 63 List of asset codes that support institution_rates. 64 65 Examples: 66 >>> import borsapy as bp 67 >>> bp.metal_institutions() 68 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 69 """ 70 return get_dovizcom_provider().get_metal_institutions()
Get list of supported precious metal assets for institution rates.
Returns: List of asset codes that support institution_rates.
Examples:
import borsapy as bp bp.metal_institutions() ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']
150def crypto_pairs(quote: str = "TRY") -> list[str]: 151 """ 152 Get list of available cryptocurrency trading pairs. 153 154 Args: 155 quote: Quote currency filter (TRY, USDT, BTC) 156 157 Returns: 158 List of available trading pair symbols. 159 160 Examples: 161 >>> import borsapy as bp 162 >>> bp.crypto_pairs() 163 ['BTCTRY', 'ETHTRY', 'XRPTRY', ...] 164 >>> bp.crypto_pairs("USDT") 165 ['BTCUSDT', 'ETHUSDT', ...] 166 """ 167 provider = get_btcturk_provider() 168 return provider.get_pairs(quote)
Get list of available cryptocurrency trading pairs.
Args: quote: Quote currency filter (TRY, USDT, BTC)
Returns: List of available trading pair symbols.
Examples:
import borsapy as bp bp.crypto_pairs() ['BTCTRY', 'ETHTRY', 'XRPTRY', ...] bp.crypto_pairs("USDT") ['BTCUSDT', 'ETHUSDT', ...]
486def search_funds(query: str, limit: int = 20) -> list[dict[str, Any]]: 487 """ 488 Search for funds by name or code. 489 490 Args: 491 query: Search query (fund code or name) 492 limit: Maximum number of results 493 494 Returns: 495 List of matching funds with fund_code, name, fund_type, return_1y. 496 497 Examples: 498 >>> import borsapy as bp 499 >>> bp.search_funds("ak portföy") 500 [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...] 501 >>> bp.search_funds("TTE") 502 [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}] 503 """ 504 provider = get_tefas_provider() 505 return provider.search(query, limit)
Search for funds by name or code.
Args: query: Search query (fund code or name) limit: Maximum number of results
Returns: List of matching funds with fund_code, name, fund_type, return_1y.
Examples:
import borsapy as bp bp.search_funds("ak portföy") [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...] bp.search_funds("TTE") [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}]
508def screen_funds( 509 fund_type: str = "YAT", 510 founder: str | None = None, 511 min_return_1m: float | None = None, 512 min_return_3m: float | None = None, 513 min_return_6m: float | None = None, 514 min_return_ytd: float | None = None, 515 min_return_1y: float | None = None, 516 min_return_3y: float | None = None, 517 limit: int = 50, 518) -> pd.DataFrame: 519 """ 520 Screen funds based on fund type and return criteria. 521 522 Args: 523 fund_type: Fund type filter: 524 - "YAT": Investment Funds (Yatırım Fonları) - default 525 - "EMK": Pension Funds (Emeklilik Fonları) 526 founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP") 527 min_return_1m: Minimum 1-month return (%) 528 min_return_3m: Minimum 3-month return (%) 529 min_return_6m: Minimum 6-month return (%) 530 min_return_ytd: Minimum year-to-date return (%) 531 min_return_1y: Minimum 1-year return (%) 532 min_return_3y: Minimum 3-year return (%) 533 limit: Maximum number of results (default: 50) 534 535 Returns: 536 DataFrame with funds matching the criteria, sorted by 1-year return. 537 538 Examples: 539 >>> import borsapy as bp 540 >>> bp.screen_funds(fund_type="EMK") # All pension funds 541 fund_code name return_1y ... 542 543 >>> bp.screen_funds(min_return_1y=50) # Funds with >50% 1Y return 544 fund_code name return_1y ... 545 546 >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20) 547 fund_code name return_ytd ... 548 """ 549 provider = get_tefas_provider() 550 results = provider.screen_funds( 551 fund_type=fund_type, 552 founder=founder, 553 min_return_1m=min_return_1m, 554 min_return_3m=min_return_3m, 555 min_return_6m=min_return_6m, 556 min_return_ytd=min_return_ytd, 557 min_return_1y=min_return_1y, 558 min_return_3y=min_return_3y, 559 limit=limit, 560 ) 561 562 if not results: 563 return pd.DataFrame(columns=["fund_code", "name", "fund_type", "return_1y"]) 564 565 return pd.DataFrame(results)
Screen funds based on fund type and return criteria.
Args: fund_type: Fund type filter: - "YAT": Investment Funds (Yatırım Fonları) - default - "EMK": Pension Funds (Emeklilik Fonları) founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP") min_return_1m: Minimum 1-month return (%) min_return_3m: Minimum 3-month return (%) min_return_6m: Minimum 6-month return (%) min_return_ytd: Minimum year-to-date return (%) min_return_1y: Minimum 1-year return (%) min_return_3y: Minimum 3-year return (%) limit: Maximum number of results (default: 50)
Returns: DataFrame with funds matching the criteria, sorted by 1-year return.
Examples:
import borsapy as bp bp.screen_funds(fund_type="EMK") # All pension funds fund_code name return_1y ...
>>> bp.screen_funds(min_return_1y=50) # Funds with >50% 1Y return fund_code name return_1y ... >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20) fund_code name return_ytd ...
568def compare_funds(fund_codes: list[str]) -> dict[str, Any]: 569 """ 570 Compare multiple funds side by side. 571 572 Args: 573 fund_codes: List of TEFAS fund codes to compare (max 10) 574 575 Returns: 576 Dictionary with: 577 - funds: List of fund details with performance metrics 578 - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc) 579 - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size) 580 581 Examples: 582 >>> import borsapy as bp 583 >>> result = bp.compare_funds(["AAK", "TTE", "YAF"]) 584 >>> result['rankings']['by_return_1y'] 585 ['TTE', 'YAF', 'AAK'] 586 587 >>> result['summary'] 588 {'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...} 589 590 >>> for fund in result['funds']: 591 ... print(f"{fund['fund_code']}: {fund['return_1y']}%") 592 AAK: 32.5% 593 TTE: 72.1% 594 YAF: 31.0% 595 """ 596 provider = get_tefas_provider() 597 return provider.compare_funds(fund_codes)
Compare multiple funds side by side.
Args: fund_codes: List of TEFAS fund codes to compare (max 10)
Returns: Dictionary with: - funds: List of fund details with performance metrics - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc) - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size)
Examples:
import borsapy as bp result = bp.compare_funds(["AAK", "TTE", "YAF"]) result['rankings']['by_return_1y'] ['TTE', 'YAF', 'AAK']
>>> result['summary'] {'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...} >>> for fund in result['funds']: ... print(f"{fund['fund_code']}: {fund['return_1y']}%") AAK: 32.5% TTE: 72.1% YAF: 31.0%
104def download( 105 tickers: str | list[str], 106 period: str = "1mo", 107 interval: str = "1d", 108 start: datetime | str | None = None, 109 end: datetime | str | None = None, 110 group_by: str = "column", 111 progress: bool = True, 112) -> pd.DataFrame: 113 """ 114 Download historical data for multiple tickers. 115 116 Similar to yfinance.download(), this function fetches OHLCV data 117 for multiple stocks and returns a DataFrame with multi-level columns. 118 119 Args: 120 tickers: Space-separated string or list of symbols. 121 Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN"] 122 period: Data period. Valid values: 123 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. 124 Ignored if start is provided. 125 interval: Data interval. Valid values: 126 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 127 start: Start date (string YYYY-MM-DD or datetime). 128 end: End date (string YYYY-MM-DD or datetime). Defaults to today. 129 group_by: How to group the output columns: 130 - 'column': MultiIndex (Price, Symbol) - default 131 - 'ticker': MultiIndex (Symbol, Price) 132 progress: Show progress (not implemented, for yfinance compatibility). 133 134 Returns: 135 DataFrame with OHLCV data. 136 - If single ticker: Simple columns (Open, High, Low, Close, Volume) 137 - If multiple tickers: MultiIndex columns based on group_by 138 139 Examples: 140 >>> import borsapy as bp 141 142 # Single ticker (returns simple DataFrame) 143 >>> bp.download("THYAO", period="1mo") 144 Open High Low Close Volume 145 Date 146 2024-12-01 265.00 268.00 264.00 267.50 12345678 147 148 # Multiple tickers (returns MultiIndex DataFrame) 149 >>> bp.download(["THYAO", "GARAN"], period="1mo") 150 Open High ... 151 THYAO GARAN THYAO GARAN 152 Date 153 2024-12-01 265.00 45.50 268.00 46.20 154 155 # With date range 156 >>> bp.download("THYAO GARAN AKBNK", start="2024-01-01", end="2024-06-30") 157 158 # Group by ticker 159 >>> bp.download(["THYAO", "GARAN"], group_by="ticker") 160 THYAO GARAN 161 Open High Low Close Open High 162 Date 163 2024-12-01 265.00 268.00 ... 45.50 46.20 164 """ 165 # Parse symbols 166 if isinstance(tickers, str): 167 symbols = [s.strip().upper() for s in tickers.split() if s.strip()] 168 else: 169 symbols = [s.strip().upper() for s in tickers if s.strip()] 170 171 if not symbols: 172 raise ValueError("No symbols provided") 173 174 # Parse dates 175 start_dt = _parse_date(start) if start else None 176 end_dt = _parse_date(end) if end else None 177 178 provider = get_tradingview_provider() 179 180 # Fetch data for each symbol 181 data_frames: dict[str, pd.DataFrame] = {} 182 for symbol in symbols: 183 try: 184 df = provider.get_history( 185 symbol=symbol, 186 period=period, 187 interval=interval, 188 start=start_dt, 189 end=end_dt, 190 ) 191 if not df.empty: 192 data_frames[symbol] = df 193 except Exception: 194 # Skip failed symbols silently (yfinance behavior) 195 continue 196 197 if not data_frames: 198 return pd.DataFrame() 199 200 # Single ticker - return simple DataFrame 201 if len(symbols) == 1 and len(data_frames) == 1: 202 return list(data_frames.values())[0] 203 204 # Multiple tickers - create MultiIndex DataFrame 205 if group_by == "ticker": 206 # Group by ticker first: (THYAO, Open), (THYAO, High), ... 207 result = pd.concat(data_frames, axis=1) 208 # result columns are already (symbol, price) 209 else: 210 # Group by column first: (Open, THYAO), (Open, GARAN), ... 211 result = pd.concat(data_frames, axis=1) 212 # Swap levels to get (price, symbol) 213 result = result.swaplevel(axis=1) 214 result = result.sort_index(axis=1, level=0) 215 216 return result
Download historical data for multiple tickers.
Similar to yfinance.download(), this function fetches OHLCV data for multiple stocks and returns a DataFrame with multi-level columns.
Args: tickers: Space-separated string or list of symbols. Example: "THYAO GARAN AKBNK" or ["THYAO", "GARAN"] period: Data period. Valid values: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. Ignored if start is provided. interval: Data interval. Valid values: 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. start: Start date (string YYYY-MM-DD or datetime). end: End date (string YYYY-MM-DD or datetime). Defaults to today. group_by: How to group the output columns: - 'column': MultiIndex (Price, Symbol) - default - 'ticker': MultiIndex (Symbol, Price) progress: Show progress (not implemented, for yfinance compatibility).
Returns: DataFrame with OHLCV data. - If single ticker: Simple columns (Open, High, Low, Close, Volume) - If multiple tickers: MultiIndex columns based on group_by
Examples:
import borsapy as bp
# Single ticker (returns simple DataFrame) >>> bp.download("THYAO", period="1mo") Open High Low Close Volume Date 2024-12-01 265.00 268.00 264.00 267.50 12345678 # Multiple tickers (returns MultiIndex DataFrame) >>> bp.download(["THYAO", "GARAN"], period="1mo") Open High ... THYAO GARAN THYAO GARAN Date 2024-12-01 265.00 45.50 268.00 46.20 # With date range >>> bp.download("THYAO GARAN AKBNK", start="2024-01-01", end="2024-06-30") # Group by ticker >>> bp.download(["THYAO", "GARAN"], group_by="ticker") THYAO GARAN Open High Low Close Open High Date 2024-12-01 265.00 268.00 ... 45.50 46.20
307def index(symbol: str) -> Index: 308 """ 309 Get an Index object for the given symbol. 310 311 This is a convenience function that creates an Index object. 312 313 Args: 314 symbol: Index symbol (e.g., "XU100", "XBANK"). 315 316 Returns: 317 Index object. 318 319 Examples: 320 >>> import borsapy as bp 321 >>> xu100 = bp.index("XU100") 322 >>> xu100.history(period="1mo") 323 """ 324 return Index(symbol)
Get an Index object for the given symbol.
This is a convenience function that creates an Index object.
Args: symbol: Index symbol (e.g., "XU100", "XBANK").
Returns: Index object.
Examples:
import borsapy as bp xu100 = bp.index("XU100") xu100.history(period="1mo")
250def indices(detailed: bool = False) -> list[str] | list[dict[str, Any]]: 251 """ 252 Get list of available market indices. 253 254 Args: 255 detailed: If True, return list of dicts with symbol, name, and count. 256 If False (default), return just the symbol list. 257 258 Returns: 259 List of index symbols, or list of dicts if detailed=True. 260 261 Examples: 262 >>> import borsapy as bp 263 >>> bp.indices() 264 ['XU100', 'XU050', 'XU030', 'XBANK', 'XUSIN', ...] 265 >>> bp.indices(detailed=True) 266 [{'symbol': 'XU100', 'name': 'BIST 100', 'count': 100}, ...] 267 """ 268 if not detailed: 269 return list(INDICES.keys()) 270 271 # Get component counts from provider 272 provider = get_bist_index_provider() 273 available = provider.get_available_indices() 274 275 # Create lookup for counts 276 count_map = {item["symbol"]: item["count"] for item in available} 277 278 result = [] 279 for symbol, name in INDICES.items(): 280 result.append({ 281 "symbol": symbol, 282 "name": name, 283 "count": count_map.get(symbol, 0), 284 }) 285 return result
Get list of available market indices.
Args: detailed: If True, return list of dicts with symbol, name, and count. If False (default), return just the symbol list.
Returns: List of index symbols, or list of dicts if detailed=True.
Examples:
import borsapy as bp bp.indices() ['XU100', 'XU050', 'XU030', 'XBANK', 'XUSIN', ...] bp.indices(detailed=True) [{'symbol': 'XU100', 'name': 'BIST 100', 'count': 100}, ...]
288def all_indices() -> list[dict[str, Any]]: 289 """ 290 Get all indices from BIST with component counts. 291 292 This returns all 79 indices available in the BIST data, 293 not just the commonly used ones in indices(). 294 295 Returns: 296 List of dicts with 'symbol', 'name', and 'count' keys. 297 298 Examples: 299 >>> import borsapy as bp 300 >>> bp.all_indices() 301 [{'symbol': 'X030C', 'name': 'BIST 30 Capped', 'count': 30}, ...] 302 """ 303 provider = get_bist_index_provider() 304 return provider.get_available_indices()
Get all indices from BIST with component counts.
This returns all 79 indices available in the BIST data, not just the commonly used ones in indices().
Returns: List of dicts with 'symbol', 'name', and 'count' keys.
Examples:
import borsapy as bp bp.all_indices() [{'symbol': 'X030C', 'name': 'BIST 30 Capped', 'count': 30}, ...]
110def bonds() -> pd.DataFrame: 111 """ 112 Get all Turkish government bond yields. 113 114 Returns: 115 DataFrame with columns: name, maturity, yield, change, change_pct. 116 117 Examples: 118 >>> import borsapy as bp 119 >>> bp.bonds() 120 name maturity yield change change_pct 121 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54 122 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31 123 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52 124 """ 125 provider = get_tahvil_provider() 126 data = provider.get_bond_yields() 127 128 if not data: 129 return pd.DataFrame(columns=["name", "maturity", "yield", "change", "change_pct"]) 130 131 df = pd.DataFrame(data) 132 133 # Select and rename columns 134 columns = { 135 "name": "name", 136 "maturity": "maturity", 137 "yield": "yield", 138 "change": "change", 139 "change_pct": "change_pct", 140 } 141 142 df = df[[c for c in columns.keys() if c in df.columns]] 143 df = df.rename(columns=columns) 144 145 return df
Get all Turkish government bond yields.
Returns: DataFrame with columns: name, maturity, yield, change, change_pct.
Examples:
import borsapy as bp bp.bonds() name maturity yield change change_pct 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
148def risk_free_rate() -> float | None: 149 """ 150 Get the risk-free rate for Turkish market (10Y bond yield). 151 152 Returns: 153 10-year government bond yield as decimal. 154 Useful for CAPM and DCF calculations. 155 156 Examples: 157 >>> import borsapy as bp 158 >>> rfr = bp.risk_free_rate() 159 >>> rfr 160 0.2803 161 """ 162 provider = get_tahvil_provider() 163 return provider.get_10y_yield()
Get the risk-free rate for Turkish market (10Y bond yield).
Returns: 10-year government bond yield as decimal. Useful for CAPM and DCF calculations.
Examples:
import borsapy as bp rfr = bp.risk_free_rate() rfr 0.2803
138def eurobonds(currency: str | None = None) -> pd.DataFrame: 139 """Get all Turkish sovereign Eurobonds as DataFrame. 140 141 Args: 142 currency: Optional filter by currency ("USD" or "EUR"). 143 144 Returns: 145 DataFrame with columns: isin, maturity, days_to_maturity, 146 currency, bid_price, bid_yield, ask_price, ask_yield. 147 148 Examples: 149 >>> import borsapy as bp 150 >>> bp.eurobonds() # All Eurobonds 151 >>> bp.eurobonds(currency="USD") # USD bonds only 152 >>> bp.eurobonds(currency="EUR") # EUR bonds only 153 """ 154 provider = get_eurobond_provider() 155 data = provider.get_eurobonds(currency=currency) 156 157 if not data: 158 return pd.DataFrame( 159 columns=[ 160 "isin", 161 "maturity", 162 "days_to_maturity", 163 "currency", 164 "bid_price", 165 "bid_yield", 166 "ask_price", 167 "ask_yield", 168 ] 169 ) 170 171 df = pd.DataFrame(data) 172 df = df.sort_values("maturity") 173 return df
Get all Turkish sovereign Eurobonds as DataFrame.
Args: currency: Optional filter by currency ("USD" or "EUR").
Returns: DataFrame with columns: isin, maturity, days_to_maturity, currency, bid_price, bid_yield, ask_price, ask_yield.
Examples:
import borsapy as bp bp.eurobonds() # All Eurobonds bp.eurobonds(currency="USD") # USD bonds only bp.eurobonds(currency="EUR") # EUR bonds only
190def policy_rate() -> float | None: 191 """Get current TCMB policy rate (1-week repo). 192 193 This is a shortcut function for quick access to the 194 main policy interest rate. 195 196 Returns: 197 Policy rate as percentage (e.g., 38.0 for 38%). 198 199 Example: 200 >>> import borsapy as bp 201 >>> bp.policy_rate() 202 38.0 203 """ 204 return TCMB().policy_rate
Get current TCMB policy rate (1-week repo).
This is a shortcut function for quick access to the main policy interest rate.
Returns: Policy rate as percentage (e.g., 38.0 for 38%).
Example:
import borsapy as bp bp.policy_rate() 38.0
250def economic_calendar( 251 period: str = "1w", 252 country: str | list[str] | None = None, 253 importance: str | None = None, 254) -> pd.DataFrame: 255 """ 256 Get economic calendar events (convenience function). 257 258 Args: 259 period: Time period (1d, 1w, 2w, 1mo). Defaults to 1w. 260 country: Country code(s) to filter by. Defaults to ['TR', 'US']. 261 importance: Filter by importance level ('low', 'mid', 'high'). 262 263 Returns: 264 DataFrame with economic events. 265 266 Examples: 267 >>> import borsapy as bp 268 >>> bp.economic_calendar() # This week, TR + US 269 >>> bp.economic_calendar(country="TR", importance="high") 270 """ 271 cal = EconomicCalendar() 272 return cal.events(period=period, country=country, importance=importance)
Get economic calendar events (convenience function).
Args: period: Time period (1d, 1w, 2w, 1mo). Defaults to 1w. country: Country code(s) to filter by. Defaults to ['TR', 'US']. importance: Filter by importance level ('low', 'mid', 'high').
Returns: DataFrame with economic events.
Examples:
import borsapy as bp bp.economic_calendar() # This week, TR + US bp.economic_calendar(country="TR", importance="high")
258def screen_stocks( 259 template: str | None = None, 260 sector: str | None = None, 261 index: str | None = None, 262 recommendation: str | None = None, 263 # Common filters as direct parameters 264 market_cap_min: float | None = None, 265 market_cap_max: float | None = None, 266 pe_min: float | None = None, 267 pe_max: float | None = None, 268 pb_min: float | None = None, 269 pb_max: float | None = None, 270 dividend_yield_min: float | None = None, 271 dividend_yield_max: float | None = None, 272 upside_potential_min: float | None = None, 273 upside_potential_max: float | None = None, 274 net_margin_min: float | None = None, 275 net_margin_max: float | None = None, 276 roe_min: float | None = None, 277 roe_max: float | None = None, 278) -> pd.DataFrame: 279 """ 280 Screen BIST stocks based on criteria (convenience function). 281 282 Args: 283 template: Pre-defined template name: 284 - "small_cap": Market cap < $1B 285 - "mid_cap": Market cap $1B-$5B 286 - "large_cap": Market cap > $5B 287 - "high_dividend": Dividend yield > 2% 288 - "high_upside": Positive upside potential 289 - "buy_recommendation": BUY recommendations 290 - "sell_recommendation": SELL recommendations 291 - "high_net_margin": Net margin > 10% 292 - "high_return": Positive weekly return 293 sector: Sector filter (e.g., "Bankacılık"). 294 index: Index filter (e.g., "BIST30"). 295 recommendation: "AL", "SAT", or "TUT". 296 market_cap_min/max: Market cap in million USD. 297 pe_min/max: P/E ratio. 298 pb_min/max: P/B ratio. 299 dividend_yield_min/max: Dividend yield (%). 300 upside_potential_min/max: Upside potential (%). 301 net_margin_min/max: Net margin (%). 302 roe_min/max: Return on equity (%). 303 304 Returns: 305 DataFrame with matching stocks. 306 307 Examples: 308 >>> import borsapy as bp 309 310 >>> # Using template 311 >>> bp.screen_stocks(template="high_dividend") 312 313 >>> # Custom filters 314 >>> bp.screen_stocks(market_cap_min=1000, pe_max=15) 315 316 >>> # Combined 317 >>> bp.screen_stocks( 318 ... sector="Bankacılık", 319 ... dividend_yield_min=3, 320 ... pe_max=10 321 ... ) 322 """ 323 screener = Screener() 324 325 # Set sector/index/recommendation 326 if sector: 327 screener.set_sector(sector) 328 if index: 329 screener.set_index(index) 330 if recommendation: 331 screener.set_recommendation(recommendation) 332 333 # Add filters 334 if market_cap_min is not None or market_cap_max is not None: 335 screener.add_filter("market_cap", min=market_cap_min, max=market_cap_max) 336 337 if pe_min is not None or pe_max is not None: 338 screener.add_filter("pe", min=pe_min, max=pe_max) 339 340 if pb_min is not None or pb_max is not None: 341 screener.add_filter("pb", min=pb_min, max=pb_max) 342 343 if dividend_yield_min is not None or dividend_yield_max is not None: 344 screener.add_filter("dividend_yield", min=dividend_yield_min, max=dividend_yield_max) 345 346 if upside_potential_min is not None or upside_potential_max is not None: 347 screener.add_filter("upside_potential", min=upside_potential_min, max=upside_potential_max) 348 349 if net_margin_min is not None or net_margin_max is not None: 350 screener.add_filter("net_margin", min=net_margin_min, max=net_margin_max) 351 352 if roe_min is not None or roe_max is not None: 353 screener.add_filter("roe", min=roe_min, max=roe_max) 354 355 return screener.run(template=template)
Screen BIST stocks based on criteria (convenience function).
Args: template: Pre-defined template name: - "small_cap": Market cap < $1B - "mid_cap": Market cap $1B-$5B - "large_cap": Market cap > $5B - "high_dividend": Dividend yield > 2% - "high_upside": Positive upside potential - "buy_recommendation": BUY recommendations - "sell_recommendation": SELL recommendations - "high_net_margin": Net margin > 10% - "high_return": Positive weekly return sector: Sector filter (e.g., "Bankacılık"). index: Index filter (e.g., "BIST30"). recommendation: "AL", "SAT", or "TUT". market_cap_min/max: Market cap in million USD. pe_min/max: P/E ratio. pb_min/max: P/B ratio. dividend_yield_min/max: Dividend yield (%). upside_potential_min/max: Upside potential (%). net_margin_min/max: Net margin (%). roe_min/max: Return on equity (%).
Returns: DataFrame with matching stocks.
Examples:
import borsapy as bp
>>> # Using template >>> bp.screen_stocks(template="high_dividend") >>> # Custom filters >>> bp.screen_stocks(market_cap_min=1000, pe_max=15) >>> # Combined >>> bp.screen_stocks( ... sector="Bankacılık", ... dividend_yield_min=3, ... pe_max=10 ... )
358def screener_criteria() -> list[dict[str, Any]]: 359 """ 360 Get list of available screening criteria. 361 362 Returns: 363 List of criteria with id, name, min, max values. 364 365 Examples: 366 >>> import borsapy as bp 367 >>> bp.screener_criteria() 368 [{'id': '7', 'name': 'Kapanış (TL)', 'min': '1.1', 'max': '14087.5'}, ...] 369 """ 370 provider = get_screener_provider() 371 return provider.get_criteria()
Get list of available screening criteria.
Returns: List of criteria with id, name, min, max values.
Examples:
import borsapy as bp bp.screener_criteria() [{'id': '7', 'name': 'Kapanış (TL)', 'min': '1.1', 'max': '14087.5'}, ...]
374def sectors() -> list[str]: 375 """ 376 Get list of available sectors for screening. 377 378 Returns: 379 List of sector names. 380 381 Examples: 382 >>> import borsapy as bp 383 >>> bp.sectors() 384 ['Bankacılık', 'Holding', 'Enerji', ...] 385 """ 386 provider = get_screener_provider() 387 data = provider.get_sectors() 388 return [item["name"] for item in data if item.get("name")]
Get list of available sectors for screening.
Returns: List of sector names.
Examples:
import borsapy as bp bp.sectors() ['Bankacılık', 'Holding', 'Enerji', ...]
391def stock_indices() -> list[str]: 392 """ 393 Get list of available indices for screening. 394 395 Returns: 396 List of index names. 397 398 Examples: 399 >>> import borsapy as bp 400 >>> bp.stock_indices() 401 ['BIST30', 'BIST100', 'BIST BANKA', ...] 402 """ 403 provider = get_screener_provider() 404 data = provider.get_indices() 405 return [item["name"] for item in data if item.get("name")]
Get list of available indices for screening.
Returns: List of index names.
Examples:
import borsapy as bp bp.stock_indices() ['BIST30', 'BIST100', 'BIST BANKA', ...]
138class TechnicalScanner: 139 """Scanner for technical analysis conditions using TradingView API. 140 141 Provides a fluent API for building and executing stock scans based on 142 technical indicators. All filtering is done server-side by TradingView 143 for optimal performance. 144 145 Examples: 146 >>> scanner = TechnicalScanner() 147 >>> scanner.set_universe("XU030") 148 >>> scanner.add_condition("rsi < 30", name="oversold") 149 >>> scanner.add_condition("volume > 1M", name="high_vol") 150 >>> results = scanner.run() 151 >>> print(results) 152 153 Supported Timeframes: 154 "1m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1W", "1M" 155 """ 156 157 def __init__(self) -> None: 158 """Initialize scanner.""" 159 self._provider = get_tv_screener_provider() 160 self._symbols: list[str] = [] 161 self._conditions: list[str] = [] 162 self._condition_names: dict[str, str] = {} # condition -> name 163 self._interval: str = "1d" 164 self._extra_columns: list[str] = [] 165 166 def set_universe(self, universe: str | list[str]) -> TechnicalScanner: 167 """Set the universe of symbols to scan. 168 169 Args: 170 universe: Index symbol (e.g., "XU030", "XU100", "XBANK") or list of stock symbols 171 172 Returns: 173 Self for method chaining 174 175 Examples: 176 >>> scanner.set_universe("XU030") # BIST 30 components 177 >>> scanner.set_universe(["THYAO", "GARAN", "ASELS"]) # Specific symbols 178 """ 179 if isinstance(universe, str): 180 # Check if it's an index 181 if universe.upper().startswith("X"): 182 from borsapy.index import Index 183 184 try: 185 idx = Index(universe.upper()) 186 self._symbols = idx.component_symbols 187 except Exception: 188 # Not a valid index, treat as single symbol 189 self._symbols = [universe.upper()] 190 else: 191 self._symbols = [universe.upper()] 192 else: 193 self._symbols = [s.upper() for s in universe] 194 return self 195 196 def add_symbol(self, symbol: str) -> TechnicalScanner: 197 """Add a single symbol to the universe. 198 199 Args: 200 symbol: Stock symbol to add 201 202 Returns: 203 Self for method chaining 204 """ 205 symbol = symbol.upper() 206 if symbol not in self._symbols: 207 self._symbols.append(symbol) 208 return self 209 210 def remove_symbol(self, symbol: str) -> TechnicalScanner: 211 """Remove a symbol from the universe. 212 213 Args: 214 symbol: Stock symbol to remove 215 216 Returns: 217 Self for method chaining 218 """ 219 symbol = symbol.upper() 220 if symbol in self._symbols: 221 self._symbols.remove(symbol) 222 return self 223 224 def add_condition( 225 self, condition: str, name: str | None = None 226 ) -> TechnicalScanner: 227 """Add a scanning condition. 228 229 Conditions are combined with AND logic. For OR logic, use the full 230 condition string: "(rsi < 30 or rsi > 70)". 231 232 Args: 233 condition: Condition string (e.g., "rsi < 30", "close > sma_50") 234 name: Optional name for the condition (for reporting) 235 236 Returns: 237 Self for method chaining 238 239 Supported Syntax: 240 - Simple: "rsi < 30", "volume > 1M" 241 - Field comparison: "close > sma_50", "macd > signal" 242 - Compound (AND): "rsi < 30 and volume > 1M" 243 - Crossover: "sma_20 crosses_above sma_50" 244 245 Examples: 246 >>> scanner.add_condition("rsi < 30", name="oversold") 247 >>> scanner.add_condition("volume > 1M", name="high_volume") 248 """ 249 # Split by "and" for multiple conditions 250 parts = [c.strip() for c in condition.lower().split(" and ")] 251 252 for part in parts: 253 if part and part not in self._conditions: 254 self._conditions.append(part) 255 cond_name = name if name and len(parts) == 1 else part 256 self._condition_names[part] = cond_name 257 258 return self 259 260 def remove_condition(self, name_or_condition: str) -> TechnicalScanner: 261 """Remove a condition by name or condition string. 262 263 Args: 264 name_or_condition: Condition name or string to remove 265 266 Returns: 267 Self for method chaining 268 """ 269 # Try to find by name 270 for cond, cname in list(self._condition_names.items()): 271 if cname == name_or_condition or cond == name_or_condition.lower(): 272 self._conditions.remove(cond) 273 del self._condition_names[cond] 274 break 275 return self 276 277 def clear_conditions(self) -> TechnicalScanner: 278 """Clear all conditions. 279 280 Returns: 281 Self for method chaining 282 """ 283 self._conditions.clear() 284 self._condition_names.clear() 285 return self 286 287 def set_interval(self, interval: str) -> TechnicalScanner: 288 """Set the data interval/timeframe for indicators. 289 290 Args: 291 interval: Timeframe for indicators: 292 - "1m", "5m", "15m", "30m" (intraday minutes) 293 - "1h", "2h", "4h" (intraday hours) 294 - "1d" (daily, default) 295 - "1W", "1wk" (weekly) 296 - "1M", "1mo" (monthly) 297 298 Returns: 299 Self for method chaining 300 """ 301 self._interval = interval 302 return self 303 304 def add_column(self, column: str) -> TechnicalScanner: 305 """Add extra column to retrieve in results. 306 307 Args: 308 column: Column name (e.g., "ema_200", "adx") 309 310 Returns: 311 Self for method chaining 312 """ 313 if column not in self._extra_columns: 314 self._extra_columns.append(column) 315 return self 316 317 def run(self, limit: int = 100) -> pd.DataFrame: 318 """Execute the scan and return results. 319 320 Args: 321 limit: Maximum number of results 322 323 Returns: 324 DataFrame with matching symbols and their data. 325 Columns include: symbol, close, volume, change, market_cap, 326 plus any indicator columns used in conditions. 327 328 Raises: 329 ValueError: If no symbols or conditions are set 330 """ 331 if not self._symbols: 332 return pd.DataFrame() 333 334 if not self._conditions: 335 return pd.DataFrame() 336 337 # Execute scan via provider 338 df = self._provider.scan( 339 symbols=self._symbols, 340 conditions=self._conditions, 341 columns=self._extra_columns, 342 interval=self._interval, 343 limit=limit, 344 ) 345 346 # Add conditions_met column for compatibility 347 if not df.empty: 348 df["conditions_met"] = [list(self._condition_names.values())] * len(df) 349 350 return df 351 352 @property 353 def symbols(self) -> list[str]: 354 """Get current symbol universe.""" 355 return self._symbols.copy() 356 357 @property 358 def conditions(self) -> list[str]: 359 """Get current conditions.""" 360 return self._conditions.copy() 361 362 # Backward compatibility aliases 363 def set_data_period(self, period: str = "3mo") -> TechnicalScanner: 364 """Deprecated: Period is not used with TradingView API.""" 365 import warnings 366 367 warnings.warn( 368 "set_data_period() is deprecated. TradingView API uses real-time data.", 369 DeprecationWarning, 370 stacklevel=2, 371 ) 372 return self 373 374 @property 375 def results(self) -> list[ScanResult]: 376 """Deprecated: Use run() which returns DataFrame directly.""" 377 return [] 378 379 def to_dataframe(self) -> pd.DataFrame: 380 """Deprecated: Use run() which returns DataFrame directly.""" 381 return self.run() 382 383 def on_match(self, callback) -> None: 384 """Deprecated: Callbacks not supported with batch API.""" 385 import warnings 386 387 warnings.warn( 388 "on_match() is deprecated. Use run() and iterate results.", 389 DeprecationWarning, 390 stacklevel=2, 391 ) 392 393 def on_scan_complete(self, callback) -> None: 394 """Deprecated: Callbacks not supported with batch API.""" 395 import warnings 396 397 warnings.warn( 398 "on_scan_complete() is deprecated. Use run() directly.", 399 DeprecationWarning, 400 stacklevel=2, 401 ) 402 403 def __repr__(self) -> str: 404 return ( 405 f"TechnicalScanner(symbols={len(self._symbols)}, " 406 f"conditions={len(self._conditions)}, interval='{self._interval}')" 407 )
Scanner for technical analysis conditions using TradingView API.
Provides a fluent API for building and executing stock scans based on technical indicators. All filtering is done server-side by TradingView for optimal performance.
Examples:
scanner = TechnicalScanner() scanner.set_universe("XU030") scanner.add_condition("rsi < 30", name="oversold") scanner.add_condition("volume > 1M", name="high_vol") results = scanner.run() print(results)
Supported Timeframes: "1m", "5m", "15m", "30m", "1h", "2h", "4h", "1d", "1W", "1M"
157 def __init__(self) -> None: 158 """Initialize scanner.""" 159 self._provider = get_tv_screener_provider() 160 self._symbols: list[str] = [] 161 self._conditions: list[str] = [] 162 self._condition_names: dict[str, str] = {} # condition -> name 163 self._interval: str = "1d" 164 self._extra_columns: list[str] = []
Initialize scanner.
166 def set_universe(self, universe: str | list[str]) -> TechnicalScanner: 167 """Set the universe of symbols to scan. 168 169 Args: 170 universe: Index symbol (e.g., "XU030", "XU100", "XBANK") or list of stock symbols 171 172 Returns: 173 Self for method chaining 174 175 Examples: 176 >>> scanner.set_universe("XU030") # BIST 30 components 177 >>> scanner.set_universe(["THYAO", "GARAN", "ASELS"]) # Specific symbols 178 """ 179 if isinstance(universe, str): 180 # Check if it's an index 181 if universe.upper().startswith("X"): 182 from borsapy.index import Index 183 184 try: 185 idx = Index(universe.upper()) 186 self._symbols = idx.component_symbols 187 except Exception: 188 # Not a valid index, treat as single symbol 189 self._symbols = [universe.upper()] 190 else: 191 self._symbols = [universe.upper()] 192 else: 193 self._symbols = [s.upper() for s in universe] 194 return self
Set the universe of symbols to scan.
Args: universe: Index symbol (e.g., "XU030", "XU100", "XBANK") or list of stock symbols
Returns: Self for method chaining
Examples:
scanner.set_universe("XU030") # BIST 30 components scanner.set_universe(["THYAO", "GARAN", "ASELS"]) # Specific symbols
196 def add_symbol(self, symbol: str) -> TechnicalScanner: 197 """Add a single symbol to the universe. 198 199 Args: 200 symbol: Stock symbol to add 201 202 Returns: 203 Self for method chaining 204 """ 205 symbol = symbol.upper() 206 if symbol not in self._symbols: 207 self._symbols.append(symbol) 208 return self
Add a single symbol to the universe.
Args: symbol: Stock symbol to add
Returns: Self for method chaining
210 def remove_symbol(self, symbol: str) -> TechnicalScanner: 211 """Remove a symbol from the universe. 212 213 Args: 214 symbol: Stock symbol to remove 215 216 Returns: 217 Self for method chaining 218 """ 219 symbol = symbol.upper() 220 if symbol in self._symbols: 221 self._symbols.remove(symbol) 222 return self
Remove a symbol from the universe.
Args: symbol: Stock symbol to remove
Returns: Self for method chaining
224 def add_condition( 225 self, condition: str, name: str | None = None 226 ) -> TechnicalScanner: 227 """Add a scanning condition. 228 229 Conditions are combined with AND logic. For OR logic, use the full 230 condition string: "(rsi < 30 or rsi > 70)". 231 232 Args: 233 condition: Condition string (e.g., "rsi < 30", "close > sma_50") 234 name: Optional name for the condition (for reporting) 235 236 Returns: 237 Self for method chaining 238 239 Supported Syntax: 240 - Simple: "rsi < 30", "volume > 1M" 241 - Field comparison: "close > sma_50", "macd > signal" 242 - Compound (AND): "rsi < 30 and volume > 1M" 243 - Crossover: "sma_20 crosses_above sma_50" 244 245 Examples: 246 >>> scanner.add_condition("rsi < 30", name="oversold") 247 >>> scanner.add_condition("volume > 1M", name="high_volume") 248 """ 249 # Split by "and" for multiple conditions 250 parts = [c.strip() for c in condition.lower().split(" and ")] 251 252 for part in parts: 253 if part and part not in self._conditions: 254 self._conditions.append(part) 255 cond_name = name if name and len(parts) == 1 else part 256 self._condition_names[part] = cond_name 257 258 return self
Add a scanning condition.
Conditions are combined with AND logic. For OR logic, use the full condition string: "(rsi < 30 or rsi > 70)".
Args: condition: Condition string (e.g., "rsi < 30", "close > sma_50") name: Optional name for the condition (for reporting)
Returns: Self for method chaining
Supported Syntax: - Simple: "rsi < 30", "volume > 1M" - Field comparison: "close > sma_50", "macd > signal" - Compound (AND): "rsi < 30 and volume > 1M" - Crossover: "sma_20 crosses_above sma_50"
Examples:
scanner.add_condition("rsi < 30", name="oversold") scanner.add_condition("volume > 1M", name="high_volume")
260 def remove_condition(self, name_or_condition: str) -> TechnicalScanner: 261 """Remove a condition by name or condition string. 262 263 Args: 264 name_or_condition: Condition name or string to remove 265 266 Returns: 267 Self for method chaining 268 """ 269 # Try to find by name 270 for cond, cname in list(self._condition_names.items()): 271 if cname == name_or_condition or cond == name_or_condition.lower(): 272 self._conditions.remove(cond) 273 del self._condition_names[cond] 274 break 275 return self
Remove a condition by name or condition string.
Args: name_or_condition: Condition name or string to remove
Returns: Self for method chaining
277 def clear_conditions(self) -> TechnicalScanner: 278 """Clear all conditions. 279 280 Returns: 281 Self for method chaining 282 """ 283 self._conditions.clear() 284 self._condition_names.clear() 285 return self
Clear all conditions.
Returns: Self for method chaining
287 def set_interval(self, interval: str) -> TechnicalScanner: 288 """Set the data interval/timeframe for indicators. 289 290 Args: 291 interval: Timeframe for indicators: 292 - "1m", "5m", "15m", "30m" (intraday minutes) 293 - "1h", "2h", "4h" (intraday hours) 294 - "1d" (daily, default) 295 - "1W", "1wk" (weekly) 296 - "1M", "1mo" (monthly) 297 298 Returns: 299 Self for method chaining 300 """ 301 self._interval = interval 302 return self
Set the data interval/timeframe for indicators.
Args: interval: Timeframe for indicators: - "1m", "5m", "15m", "30m" (intraday minutes) - "1h", "2h", "4h" (intraday hours) - "1d" (daily, default) - "1W", "1wk" (weekly) - "1M", "1mo" (monthly)
Returns: Self for method chaining
304 def add_column(self, column: str) -> TechnicalScanner: 305 """Add extra column to retrieve in results. 306 307 Args: 308 column: Column name (e.g., "ema_200", "adx") 309 310 Returns: 311 Self for method chaining 312 """ 313 if column not in self._extra_columns: 314 self._extra_columns.append(column) 315 return self
Add extra column to retrieve in results.
Args: column: Column name (e.g., "ema_200", "adx")
Returns: Self for method chaining
317 def run(self, limit: int = 100) -> pd.DataFrame: 318 """Execute the scan and return results. 319 320 Args: 321 limit: Maximum number of results 322 323 Returns: 324 DataFrame with matching symbols and their data. 325 Columns include: symbol, close, volume, change, market_cap, 326 plus any indicator columns used in conditions. 327 328 Raises: 329 ValueError: If no symbols or conditions are set 330 """ 331 if not self._symbols: 332 return pd.DataFrame() 333 334 if not self._conditions: 335 return pd.DataFrame() 336 337 # Execute scan via provider 338 df = self._provider.scan( 339 symbols=self._symbols, 340 conditions=self._conditions, 341 columns=self._extra_columns, 342 interval=self._interval, 343 limit=limit, 344 ) 345 346 # Add conditions_met column for compatibility 347 if not df.empty: 348 df["conditions_met"] = [list(self._condition_names.values())] * len(df) 349 350 return df
Execute the scan and return results.
Args: limit: Maximum number of results
Returns: DataFrame with matching symbols and their data. Columns include: symbol, close, volume, change, market_cap, plus any indicator columns used in conditions.
Raises: ValueError: If no symbols or conditions are set
352 @property 353 def symbols(self) -> list[str]: 354 """Get current symbol universe.""" 355 return self._symbols.copy()
Get current symbol universe.
357 @property 358 def conditions(self) -> list[str]: 359 """Get current conditions.""" 360 return self._conditions.copy()
Get current conditions.
363 def set_data_period(self, period: str = "3mo") -> TechnicalScanner: 364 """Deprecated: Period is not used with TradingView API.""" 365 import warnings 366 367 warnings.warn( 368 "set_data_period() is deprecated. TradingView API uses real-time data.", 369 DeprecationWarning, 370 stacklevel=2, 371 ) 372 return self
Deprecated: Period is not used with TradingView API.
374 @property 375 def results(self) -> list[ScanResult]: 376 """Deprecated: Use run() which returns DataFrame directly.""" 377 return []
Deprecated: Use run() which returns DataFrame directly.
379 def to_dataframe(self) -> pd.DataFrame: 380 """Deprecated: Use run() which returns DataFrame directly.""" 381 return self.run()
Deprecated: Use run() which returns DataFrame directly.
383 def on_match(self, callback) -> None: 384 """Deprecated: Callbacks not supported with batch API.""" 385 import warnings 386 387 warnings.warn( 388 "on_match() is deprecated. Use run() and iterate results.", 389 DeprecationWarning, 390 stacklevel=2, 391 )
Deprecated: Callbacks not supported with batch API.
393 def on_scan_complete(self, callback) -> None: 394 """Deprecated: Callbacks not supported with batch API.""" 395 import warnings 396 397 warnings.warn( 398 "on_scan_complete() is deprecated. Use run() directly.", 399 DeprecationWarning, 400 stacklevel=2, 401 )
Deprecated: Callbacks not supported with batch API.
49@dataclass 50class ScanResult: 51 """Result of scanning a single symbol. 52 53 Attributes: 54 symbol: The stock symbol 55 data: Dictionary with current values and indicators 56 conditions_met: List of condition names that were satisfied 57 timestamp: When the scan was performed 58 """ 59 60 symbol: str 61 data: dict[str, Any] = field(default_factory=dict) 62 conditions_met: list[str] = field(default_factory=list) 63 timestamp: datetime = field(default_factory=datetime.now)
Result of scanning a single symbol.
Attributes: symbol: The stock symbol data: Dictionary with current values and indicators conditions_met: List of condition names that were satisfied timestamp: When the scan was performed
66def scan( 67 universe: str | list[str], 68 condition: str, 69 interval: str = "1d", 70 limit: int = 100, 71) -> pd.DataFrame: 72 """Convenience function for quick technical scanning using TradingView API. 73 74 This function provides a simple interface to scan stocks based on technical 75 conditions. All filtering is done server-side by TradingView for fast results. 76 77 Args: 78 universe: Index symbol (e.g., "XU030", "XU100", "XBANK") or list of stock symbols 79 condition: Condition string supporting: 80 - Simple comparisons: "rsi < 30", "volume > 1M" 81 - Field comparisons: "close > sma_50", "macd > signal" 82 - Compound conditions: "rsi < 30 and close > sma_50" 83 - Crossover: "sma_20 crosses_above sma_50", "macd crosses signal" 84 - Percentage: "close above_pct sma_50 1.05", "close below_pct sma_50 0.95" 85 interval: Timeframe for indicators ("1m", "5m", "15m", "30m", "1h", "4h", "1d", "1W", "1M") 86 limit: Maximum number of results (default: 100) 87 88 Returns: 89 DataFrame with matching symbols and their indicator values 90 91 Examples: 92 >>> import borsapy as bp 93 94 # RSI oversold 95 >>> bp.scan("XU030", "rsi < 30") 96 97 # Price above SMA50 98 >>> bp.scan("XU100", "close > sma_50") 99 100 # Compound condition 101 >>> bp.scan("XU030", "rsi < 30 and volume > 1M") 102 103 # MACD bullish 104 >>> bp.scan("XU100", "macd > signal") 105 106 # Golden cross 107 >>> bp.scan("XU030", "sma_20 crosses_above sma_50") 108 109 # MACD crosses signal line 110 >>> bp.scan("XU030", "macd crosses signal") 111 112 # Close 5% above SMA50 113 >>> bp.scan("XU030", "close above_pct sma_50 1.05") 114 115 # Hourly timeframe 116 >>> bp.scan("XU030", "rsi < 30", interval="1h") 117 118 Supported Fields: 119 Price: price, close, open, high, low, volume, change_percent, market_cap 120 RSI: rsi, rsi_7, rsi_14 121 SMA: sma_5, sma_10, sma_20, sma_30, sma_50, sma_100, sma_200 122 EMA: ema_5, ema_10, ema_12, ema_20, ema_26, ema_50, ema_100, ema_200 123 MACD: macd, signal, histogram 124 Stochastic: stoch_k, stoch_d 125 ADX: adx 126 Bollinger: bb_upper, bb_middle, bb_lower 127 ATR: atr 128 CCI: cci 129 Williams %R: wr 130 """ 131 scanner = TechnicalScanner() 132 scanner.set_universe(universe) 133 scanner.add_condition(condition) 134 scanner.set_interval(interval) 135 return scanner.run(limit=limit)
Convenience function for quick technical scanning using TradingView API.
This function provides a simple interface to scan stocks based on technical conditions. All filtering is done server-side by TradingView for fast results.
Args: universe: Index symbol (e.g., "XU030", "XU100", "XBANK") or list of stock symbols condition: Condition string supporting: - Simple comparisons: "rsi < 30", "volume > 1M" - Field comparisons: "close > sma_50", "macd > signal" - Compound conditions: "rsi < 30 and close > sma_50" - Crossover: "sma_20 crosses_above sma_50", "macd crosses signal" - Percentage: "close above_pct sma_50 1.05", "close below_pct sma_50 0.95" interval: Timeframe for indicators ("1m", "5m", "15m", "30m", "1h", "4h", "1d", "1W", "1M") limit: Maximum number of results (default: 100)
Returns: DataFrame with matching symbols and their indicator values
Examples:
import borsapy as bp
# RSI oversold >>> bp.scan("XU030", "rsi < 30") # Price above SMA50 >>> bp.scan("XU100", "close > sma_50") # Compound condition >>> bp.scan("XU030", "rsi < 30 and volume > 1M") # MACD bullish >>> bp.scan("XU100", "macd > signal") # Golden cross >>> bp.scan("XU030", "sma_20 crosses_above sma_50") # MACD crosses signal line >>> bp.scan("XU030", "macd crosses signal") # Close 5% above SMA50 >>> bp.scan("XU030", "close above_pct sma_50 1.05") # Hourly timeframe >>> bp.scan("XU030", "rsi < 30", interval="1h")Supported Fields: Price: price, close, open, high, low, volume, change_percent, market_cap RSI: rsi, rsi_7, rsi_14 SMA: sma_5, sma_10, sma_20, sma_30, sma_50, sma_100, sma_200 EMA: ema_5, ema_10, ema_12, ema_20, ema_26, ema_50, ema_100, ema_200 MACD: macd, signal, histogram Stochastic: stoch_k, stoch_d ADX: adx Bollinger: bb_upper, bb_middle, bb_lower ATR: atr CCI: cci Williams %R: wr
655class TechnicalAnalyzer: 656 """Technical analysis wrapper for OHLCV DataFrames. 657 658 Provides easy access to technical indicators as methods and properties. 659 660 Example: 661 >>> df = stock.history(period="1y") 662 >>> ta = TechnicalAnalyzer(df) 663 >>> ta.rsi() # Returns full RSI series 664 >>> ta.latest # Returns dict with latest values of all indicators 665 """ 666 667 def __init__(self, df: pd.DataFrame) -> None: 668 """Initialize with OHLCV DataFrame. 669 670 Args: 671 df: DataFrame with price data (must have at least 'Close' column) 672 """ 673 self._df = df.copy() 674 self._has_volume = "Volume" in df.columns 675 self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"]) 676 677 def sma(self, period: int = 20) -> pd.Series: 678 """Calculate Simple Moving Average.""" 679 return calculate_sma(self._df, period) 680 681 def ema(self, period: int = 20) -> pd.Series: 682 """Calculate Exponential Moving Average.""" 683 return calculate_ema(self._df, period) 684 685 def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series: 686 """Calculate Tilson T3 Moving Average.""" 687 return calculate_tilson_t3(self._df, period, vfactor) 688 689 def rsi(self, period: int = 14) -> pd.Series: 690 """Calculate Relative Strength Index.""" 691 return calculate_rsi(self._df, period) 692 693 def macd( 694 self, fast: int = 12, slow: int = 26, signal: int = 9 695 ) -> pd.DataFrame: 696 """Calculate MACD (line, signal, histogram).""" 697 return calculate_macd(self._df, fast, slow, signal) 698 699 def bollinger_bands( 700 self, period: int = 20, std_dev: float = 2.0 701 ) -> pd.DataFrame: 702 """Calculate Bollinger Bands (upper, middle, lower).""" 703 return calculate_bollinger_bands(self._df, period, std_dev) 704 705 def atr(self, period: int = 14) -> pd.Series: 706 """Calculate Average True Range.""" 707 return calculate_atr(self._df, period) 708 709 def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame: 710 """Calculate Stochastic Oscillator (%K, %D).""" 711 return calculate_stochastic(self._df, k_period, d_period) 712 713 def obv(self) -> pd.Series: 714 """Calculate On-Balance Volume.""" 715 return calculate_obv(self._df) 716 717 def vwap(self) -> pd.Series: 718 """Calculate Volume Weighted Average Price.""" 719 return calculate_vwap(self._df) 720 721 def adx(self, period: int = 14) -> pd.Series: 722 """Calculate Average Directional Index.""" 723 return calculate_adx(self._df, period) 724 725 def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame: 726 """Calculate Supertrend indicator. 727 728 Args: 729 atr_period: Period for ATR calculation (default 10) 730 multiplier: ATR multiplier for bands (default 3.0) 731 732 Returns: 733 DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower 734 """ 735 return calculate_supertrend(self._df, atr_period, multiplier) 736 737 def heikin_ashi(self) -> pd.DataFrame: 738 """Calculate Heikin Ashi candlestick values. 739 740 Returns: 741 DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns 742 """ 743 from borsapy.charts import calculate_heikin_ashi 744 745 return calculate_heikin_ashi(self._df) 746 747 def all(self, **kwargs: Any) -> pd.DataFrame: 748 """Get DataFrame with all applicable indicators added.""" 749 return add_indicators(self._df, **kwargs) 750 751 @property 752 def latest(self) -> dict[str, float]: 753 """Get latest values of all applicable indicators. 754 755 Returns: 756 Dictionary with indicator names and their latest values 757 """ 758 result: dict[str, float] = {} 759 760 # Always available (need Close or Price) 761 has_price = "Close" in self._df.columns or "Price" in self._df.columns 762 if has_price and len(self._df) > 0: 763 result["sma_20"] = float(self.sma(20).iloc[-1]) 764 result["sma_50"] = float(self.sma(50).iloc[-1]) 765 result["ema_12"] = float(self.ema(12).iloc[-1]) 766 result["ema_26"] = float(self.ema(26).iloc[-1]) 767 result["t3_5"] = float(self.tilson_t3(5).iloc[-1]) 768 result["rsi_14"] = float(self.rsi(14).iloc[-1]) 769 770 macd_df = self.macd() 771 result["macd"] = float(macd_df["MACD"].iloc[-1]) 772 result["macd_signal"] = float(macd_df["Signal"].iloc[-1]) 773 result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1]) 774 775 bb_df = self.bollinger_bands() 776 result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1]) 777 result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1]) 778 result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1]) 779 780 # Need High, Low, Close 781 if self._has_hlc and len(self._df) > 0: 782 result["atr_14"] = float(self.atr(14).iloc[-1]) 783 result["adx_14"] = float(self.adx(14).iloc[-1]) 784 785 stoch_df = self.stochastic() 786 result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1]) 787 result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1]) 788 789 st_df = self.supertrend() 790 result["supertrend"] = float(st_df["Supertrend"].iloc[-1]) 791 result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1]) 792 793 # Need Volume 794 if self._has_volume and len(self._df) > 0: 795 result["obv"] = float(self.obv().iloc[-1]) 796 797 # Need HLC + Volume 798 if self._has_hlc and self._has_volume and len(self._df) > 0: 799 result["vwap"] = float(self.vwap().iloc[-1]) 800 801 # Round all values 802 return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}
Technical analysis wrapper for OHLCV DataFrames.
Provides easy access to technical indicators as methods and properties.
Example:
df = stock.history(period="1y") ta = TechnicalAnalyzer(df) ta.rsi() # Returns full RSI series ta.latest # Returns dict with latest values of all indicators
667 def __init__(self, df: pd.DataFrame) -> None: 668 """Initialize with OHLCV DataFrame. 669 670 Args: 671 df: DataFrame with price data (must have at least 'Close' column) 672 """ 673 self._df = df.copy() 674 self._has_volume = "Volume" in df.columns 675 self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])
Initialize with OHLCV DataFrame.
Args: df: DataFrame with price data (must have at least 'Close' column)
677 def sma(self, period: int = 20) -> pd.Series: 678 """Calculate Simple Moving Average.""" 679 return calculate_sma(self._df, period)
Calculate Simple Moving Average.
681 def ema(self, period: int = 20) -> pd.Series: 682 """Calculate Exponential Moving Average.""" 683 return calculate_ema(self._df, period)
Calculate Exponential Moving Average.
685 def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series: 686 """Calculate Tilson T3 Moving Average.""" 687 return calculate_tilson_t3(self._df, period, vfactor)
Calculate Tilson T3 Moving Average.
689 def rsi(self, period: int = 14) -> pd.Series: 690 """Calculate Relative Strength Index.""" 691 return calculate_rsi(self._df, period)
Calculate Relative Strength Index.
693 def macd( 694 self, fast: int = 12, slow: int = 26, signal: int = 9 695 ) -> pd.DataFrame: 696 """Calculate MACD (line, signal, histogram).""" 697 return calculate_macd(self._df, fast, slow, signal)
Calculate MACD (line, signal, histogram).
699 def bollinger_bands( 700 self, period: int = 20, std_dev: float = 2.0 701 ) -> pd.DataFrame: 702 """Calculate Bollinger Bands (upper, middle, lower).""" 703 return calculate_bollinger_bands(self._df, period, std_dev)
Calculate Bollinger Bands (upper, middle, lower).
705 def atr(self, period: int = 14) -> pd.Series: 706 """Calculate Average True Range.""" 707 return calculate_atr(self._df, period)
Calculate Average True Range.
709 def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame: 710 """Calculate Stochastic Oscillator (%K, %D).""" 711 return calculate_stochastic(self._df, k_period, d_period)
Calculate Stochastic Oscillator (%K, %D).
713 def obv(self) -> pd.Series: 714 """Calculate On-Balance Volume.""" 715 return calculate_obv(self._df)
Calculate On-Balance Volume.
717 def vwap(self) -> pd.Series: 718 """Calculate Volume Weighted Average Price.""" 719 return calculate_vwap(self._df)
Calculate Volume Weighted Average Price.
721 def adx(self, period: int = 14) -> pd.Series: 722 """Calculate Average Directional Index.""" 723 return calculate_adx(self._df, period)
Calculate Average Directional Index.
725 def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame: 726 """Calculate Supertrend indicator. 727 728 Args: 729 atr_period: Period for ATR calculation (default 10) 730 multiplier: ATR multiplier for bands (default 3.0) 731 732 Returns: 733 DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower 734 """ 735 return calculate_supertrend(self._df, atr_period, multiplier)
Calculate Supertrend indicator.
Args: atr_period: Period for ATR calculation (default 10) multiplier: ATR multiplier for bands (default 3.0)
Returns: DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower
737 def heikin_ashi(self) -> pd.DataFrame: 738 """Calculate Heikin Ashi candlestick values. 739 740 Returns: 741 DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns 742 """ 743 from borsapy.charts import calculate_heikin_ashi 744 745 return calculate_heikin_ashi(self._df)
Calculate Heikin Ashi candlestick values.
Returns: DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
747 def all(self, **kwargs: Any) -> pd.DataFrame: 748 """Get DataFrame with all applicable indicators added.""" 749 return add_indicators(self._df, **kwargs)
Get DataFrame with all applicable indicators added.
751 @property 752 def latest(self) -> dict[str, float]: 753 """Get latest values of all applicable indicators. 754 755 Returns: 756 Dictionary with indicator names and their latest values 757 """ 758 result: dict[str, float] = {} 759 760 # Always available (need Close or Price) 761 has_price = "Close" in self._df.columns or "Price" in self._df.columns 762 if has_price and len(self._df) > 0: 763 result["sma_20"] = float(self.sma(20).iloc[-1]) 764 result["sma_50"] = float(self.sma(50).iloc[-1]) 765 result["ema_12"] = float(self.ema(12).iloc[-1]) 766 result["ema_26"] = float(self.ema(26).iloc[-1]) 767 result["t3_5"] = float(self.tilson_t3(5).iloc[-1]) 768 result["rsi_14"] = float(self.rsi(14).iloc[-1]) 769 770 macd_df = self.macd() 771 result["macd"] = float(macd_df["MACD"].iloc[-1]) 772 result["macd_signal"] = float(macd_df["Signal"].iloc[-1]) 773 result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1]) 774 775 bb_df = self.bollinger_bands() 776 result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1]) 777 result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1]) 778 result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1]) 779 780 # Need High, Low, Close 781 if self._has_hlc and len(self._df) > 0: 782 result["atr_14"] = float(self.atr(14).iloc[-1]) 783 result["adx_14"] = float(self.adx(14).iloc[-1]) 784 785 stoch_df = self.stochastic() 786 result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1]) 787 result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1]) 788 789 st_df = self.supertrend() 790 result["supertrend"] = float(st_df["Supertrend"].iloc[-1]) 791 result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1]) 792 793 # Need Volume 794 if self._has_volume and len(self._df) > 0: 795 result["obv"] = float(self.obv().iloc[-1]) 796 797 # Need HLC + Volume 798 if self._has_hlc and self._has_volume and len(self._df) > 0: 799 result["vwap"] = float(self.vwap().iloc[-1]) 800 801 # Round all values 802 return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}
Get latest values of all applicable indicators.
Returns: Dictionary with indicator names and their latest values
560def add_indicators( 561 df: pd.DataFrame, 562 indicators: list[str] | None = None, 563 **kwargs: Any, 564) -> pd.DataFrame: 565 """Add technical indicator columns to a DataFrame. 566 567 Args: 568 df: DataFrame with OHLCV data (Open, High, Low, Close, Volume) 569 indicators: List of indicators to add. If None, adds all applicable. 570 Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr', 571 'stochastic', 'obv', 'vwap', 'adx', 'supertrend' 572 **kwargs: Additional arguments for specific indicators: 573 - sma_period: SMA period (default 20) 574 - ema_period: EMA period (default 12) 575 - rsi_period: RSI period (default 14) 576 - bb_period: Bollinger Bands period (default 20) 577 - atr_period: ATR period (default 14) 578 - adx_period: ADX period (default 14) 579 - supertrend_period: Supertrend ATR period (default 10) 580 - supertrend_multiplier: Supertrend ATR multiplier (default 3.0) 581 582 Returns: 583 DataFrame with indicator columns added 584 """ 585 result = df.copy() 586 587 # Default indicators based on available columns 588 has_volume = "Volume" in df.columns 589 has_hlc = all(col in df.columns for col in ["High", "Low", "Close"]) 590 591 if indicators is None: 592 indicators = ["sma", "ema", "rsi", "macd", "bollinger"] 593 if has_hlc: 594 indicators.extend(["atr", "stochastic", "adx", "supertrend"]) 595 if has_volume: 596 indicators.append("obv") 597 if has_volume and has_hlc: 598 indicators.append("vwap") 599 600 # Get periods from kwargs 601 sma_period = kwargs.get("sma_period", 20) 602 ema_period = kwargs.get("ema_period", 12) 603 rsi_period = kwargs.get("rsi_period", 14) 604 bb_period = kwargs.get("bb_period", 20) 605 atr_period = kwargs.get("atr_period", 14) 606 adx_period = kwargs.get("adx_period", 14) 607 supertrend_period = kwargs.get("supertrend_period", 10) 608 supertrend_multiplier = kwargs.get("supertrend_multiplier", 3.0) 609 610 # Add indicators 611 for indicator in indicators: 612 indicator = indicator.lower() 613 614 if indicator == "sma": 615 result[f"SMA_{sma_period}"] = calculate_sma(df, sma_period) 616 elif indicator == "ema": 617 result[f"EMA_{ema_period}"] = calculate_ema(df, ema_period) 618 elif indicator == "rsi": 619 result[f"RSI_{rsi_period}"] = calculate_rsi(df, rsi_period) 620 elif indicator == "macd": 621 macd_df = calculate_macd(df) 622 result["MACD"] = macd_df["MACD"] 623 result["MACD_Signal"] = macd_df["Signal"] 624 result["MACD_Hist"] = macd_df["Histogram"] 625 elif indicator == "bollinger": 626 bb_df = calculate_bollinger_bands(df, bb_period) 627 result["BB_Upper"] = bb_df["BB_Upper"] 628 result["BB_Middle"] = bb_df["BB_Middle"] 629 result["BB_Lower"] = bb_df["BB_Lower"] 630 elif indicator == "atr" and has_hlc: 631 result[f"ATR_{atr_period}"] = calculate_atr(df, atr_period) 632 elif indicator == "stochastic" and has_hlc: 633 stoch_df = calculate_stochastic(df) 634 result["Stoch_K"] = stoch_df["Stoch_K"] 635 result["Stoch_D"] = stoch_df["Stoch_D"] 636 elif indicator == "obv" and has_volume: 637 result["OBV"] = calculate_obv(df) 638 elif indicator == "vwap" and has_volume and has_hlc: 639 result["VWAP"] = calculate_vwap(df) 640 elif indicator == "adx" and has_hlc: 641 result[f"ADX_{adx_period}"] = calculate_adx(df, adx_period) 642 elif indicator == "supertrend" and has_hlc: 643 st_df = calculate_supertrend(df, supertrend_period, supertrend_multiplier) 644 result["Supertrend"] = st_df["Supertrend"] 645 result["Supertrend_Direction"] = st_df["Supertrend_Direction"] 646 647 return result
Add technical indicator columns to a DataFrame.
Args: df: DataFrame with OHLCV data (Open, High, Low, Close, Volume) indicators: List of indicators to add. If None, adds all applicable. Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr', 'stochastic', 'obv', 'vwap', 'adx', 'supertrend' **kwargs: Additional arguments for specific indicators: - sma_period: SMA period (default 20) - ema_period: EMA period (default 12) - rsi_period: RSI period (default 14) - bb_period: Bollinger Bands period (default 20) - atr_period: ATR period (default 14) - adx_period: ADX period (default 14) - supertrend_period: Supertrend ATR period (default 10) - supertrend_multiplier: Supertrend ATR multiplier (default 3.0)
Returns: DataFrame with indicator columns added
55def calculate_sma( 56 df: pd.DataFrame, period: int = 20, column: str = "Close" 57) -> pd.Series: 58 """Calculate Simple Moving Average (SMA). 59 60 Args: 61 df: DataFrame with price data 62 period: Number of periods for moving average 63 column: Column name to use for calculation 64 65 Returns: 66 Series with SMA values 67 """ 68 col = _get_price_column(df, column) 69 if col not in df.columns: 70 return pd.Series(np.nan, index=df.index, name=f"SMA_{period}") 71 return df[col].rolling(window=period, min_periods=1).mean()
Calculate Simple Moving Average (SMA).
Args: df: DataFrame with price data period: Number of periods for moving average column: Column name to use for calculation
Returns: Series with SMA values
74def calculate_ema( 75 df: pd.DataFrame, period: int = 20, column: str = "Close" 76) -> pd.Series: 77 """Calculate Exponential Moving Average (EMA). 78 79 Args: 80 df: DataFrame with price data 81 period: Number of periods for moving average 82 column: Column name to use for calculation 83 84 Returns: 85 Series with EMA values 86 """ 87 col = _get_price_column(df, column) 88 if col not in df.columns: 89 return pd.Series(np.nan, index=df.index, name=f"EMA_{period}") 90 return df[col].ewm(span=period, adjust=False).mean()
Calculate Exponential Moving Average (EMA).
Args: df: DataFrame with price data period: Number of periods for moving average column: Column name to use for calculation
Returns: Series with EMA values
150def calculate_rsi( 151 df: pd.DataFrame, period: int = 14, column: str = "Close" 152) -> pd.Series: 153 """Calculate Relative Strength Index (RSI). 154 155 RSI measures the speed and magnitude of price movements on a scale of 0-100. 156 - RSI > 70: Overbought (potential sell signal) 157 - RSI < 30: Oversold (potential buy signal) 158 159 Args: 160 df: DataFrame with price data 161 period: Number of periods for RSI calculation (default 14) 162 column: Column name to use for calculation 163 164 Returns: 165 Series with RSI values (0-100) 166 """ 167 col = _get_price_column(df, column) 168 if col not in df.columns or len(df) < period: 169 return pd.Series(np.nan, index=df.index, name=f"RSI_{period}") 170 171 delta = df[col].diff() 172 gain = delta.where(delta > 0, 0.0) 173 loss = (-delta).where(delta < 0, 0.0) 174 175 # Use Wilder's smoothing (same as TradingView) 176 # Wilder's uses alpha=1/period, NOT span=period 177 avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() 178 avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() 179 180 rs = avg_gain / avg_loss 181 rsi = 100.0 - (100.0 / (1.0 + rs)) 182 183 # Handle division by zero 184 rsi = rsi.replace([np.inf, -np.inf], np.nan) 185 rsi = rsi.fillna(50.0) # Neutral RSI when no movement 186 187 return rsi.rename(f"RSI_{period}")
Calculate Relative Strength Index (RSI).
RSI measures the speed and magnitude of price movements on a scale of 0-100.
- RSI > 70: Overbought (potential sell signal)
- RSI < 30: Oversold (potential buy signal)
Args: df: DataFrame with price data period: Number of periods for RSI calculation (default 14) column: Column name to use for calculation
Returns: Series with RSI values (0-100)
190def calculate_macd( 191 df: pd.DataFrame, 192 fast: int = 12, 193 slow: int = 26, 194 signal: int = 9, 195 column: str = "Close", 196) -> pd.DataFrame: 197 """Calculate Moving Average Convergence Divergence (MACD). 198 199 MACD shows the relationship between two moving averages of prices. 200 - MACD Line: Fast EMA - Slow EMA 201 - Signal Line: EMA of MACD Line 202 - Histogram: MACD Line - Signal Line 203 204 Args: 205 df: DataFrame with price data 206 fast: Fast EMA period (default 12) 207 slow: Slow EMA period (default 26) 208 signal: Signal line EMA period (default 9) 209 column: Column name to use for calculation 210 211 Returns: 212 DataFrame with columns: MACD, Signal, Histogram 213 """ 214 col = _get_price_column(df, column) 215 if col not in df.columns: 216 return pd.DataFrame( 217 {"MACD": np.nan, "Signal": np.nan, "Histogram": np.nan}, 218 index=df.index, 219 ) 220 221 ema_fast = df[col].ewm(span=fast, adjust=False).mean() 222 ema_slow = df[col].ewm(span=slow, adjust=False).mean() 223 224 macd_line = ema_fast - ema_slow 225 signal_line = macd_line.ewm(span=signal, adjust=False).mean() 226 histogram = macd_line - signal_line 227 228 return pd.DataFrame( 229 {"MACD": macd_line, "Signal": signal_line, "Histogram": histogram}, 230 index=df.index, 231 )
Calculate Moving Average Convergence Divergence (MACD).
MACD shows the relationship between two moving averages of prices.
- MACD Line: Fast EMA - Slow EMA
- Signal Line: EMA of MACD Line
- Histogram: MACD Line - Signal Line
Args: df: DataFrame with price data fast: Fast EMA period (default 12) slow: Slow EMA period (default 26) signal: Signal line EMA period (default 9) column: Column name to use for calculation
Returns: DataFrame with columns: MACD, Signal, Histogram
234def calculate_bollinger_bands( 235 df: pd.DataFrame, period: int = 20, std_dev: float = 2.0, column: str = "Close" 236) -> pd.DataFrame: 237 """Calculate Bollinger Bands. 238 239 Bollinger Bands consist of a middle band (SMA) and two outer bands 240 at standard deviation levels above and below the middle band. 241 242 Args: 243 df: DataFrame with price data 244 period: Period for SMA and standard deviation 245 std_dev: Number of standard deviations for bands 246 column: Column name to use for calculation 247 248 Returns: 249 DataFrame with columns: Upper, Middle, Lower 250 """ 251 col = _get_price_column(df, column) 252 if col not in df.columns: 253 return pd.DataFrame( 254 {"BB_Upper": np.nan, "BB_Middle": np.nan, "BB_Lower": np.nan}, 255 index=df.index, 256 ) 257 258 middle = df[col].rolling(window=period, min_periods=1).mean() 259 std = df[col].rolling(window=period, min_periods=1).std() 260 261 upper = middle + (std * std_dev) 262 lower = middle - (std * std_dev) 263 264 return pd.DataFrame( 265 {"BB_Upper": upper, "BB_Middle": middle, "BB_Lower": lower}, 266 index=df.index, 267 )
Calculate Bollinger Bands.
Bollinger Bands consist of a middle band (SMA) and two outer bands at standard deviation levels above and below the middle band.
Args: df: DataFrame with price data period: Period for SMA and standard deviation std_dev: Number of standard deviations for bands column: Column name to use for calculation
Returns: DataFrame with columns: Upper, Middle, Lower
270def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series: 271 """Calculate Average True Range (ATR). 272 273 ATR measures market volatility by decomposing the entire range of an asset 274 price for that period. 275 276 Args: 277 df: DataFrame with High, Low, Close columns 278 period: Period for ATR calculation 279 280 Returns: 281 Series with ATR values 282 """ 283 required = ["High", "Low", "Close"] 284 if not all(col in df.columns for col in required): 285 return pd.Series(np.nan, index=df.index, name=f"ATR_{period}") 286 287 high = df["High"] 288 low = df["Low"] 289 close = df["Close"] 290 291 # True Range components 292 tr1 = high - low 293 tr2 = abs(high - close.shift(1)) 294 tr3 = abs(low - close.shift(1)) 295 296 # True Range is the maximum of the three 297 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 298 299 # ATR uses Wilder's smoothing (same as TradingView) 300 atr = tr.ewm(alpha=1 / period, adjust=False).mean() 301 302 return atr.rename(f"ATR_{period}")
Calculate Average True Range (ATR).
ATR measures market volatility by decomposing the entire range of an asset price for that period.
Args: df: DataFrame with High, Low, Close columns period: Period for ATR calculation
Returns: Series with ATR values
305def calculate_stochastic( 306 df: pd.DataFrame, k_period: int = 14, d_period: int = 3 307) -> pd.DataFrame: 308 """Calculate Stochastic Oscillator (%K and %D). 309 310 The Stochastic Oscillator compares a closing price to a range of prices 311 over a certain period of time. 312 - %K > 80: Overbought 313 - %K < 20: Oversold 314 315 Args: 316 df: DataFrame with High, Low, Close columns 317 k_period: Period for %K calculation 318 d_period: Period for %D (signal line) 319 320 Returns: 321 DataFrame with columns: Stoch_K, Stoch_D 322 """ 323 required = ["High", "Low", "Close"] 324 if not all(col in df.columns for col in required): 325 return pd.DataFrame( 326 {"Stoch_K": np.nan, "Stoch_D": np.nan}, 327 index=df.index, 328 ) 329 330 # Calculate %K 331 lowest_low = df["Low"].rolling(window=k_period, min_periods=1).min() 332 highest_high = df["High"].rolling(window=k_period, min_periods=1).max() 333 334 stoch_k = 100 * (df["Close"] - lowest_low) / (highest_high - lowest_low) 335 stoch_k = stoch_k.replace([np.inf, -np.inf], np.nan).fillna(50.0) 336 337 # %D is the SMA of %K 338 stoch_d = stoch_k.rolling(window=d_period, min_periods=1).mean() 339 340 return pd.DataFrame( 341 {"Stoch_K": stoch_k, "Stoch_D": stoch_d}, 342 index=df.index, 343 )
Calculate Stochastic Oscillator (%K and %D).
The Stochastic Oscillator compares a closing price to a range of prices over a certain period of time.
- %K > 80: Overbought
- %K < 20: Oversold
Args: df: DataFrame with High, Low, Close columns k_period: Period for %K calculation d_period: Period for %D (signal line)
Returns: DataFrame with columns: Stoch_K, Stoch_D
346def calculate_obv(df: pd.DataFrame) -> pd.Series: 347 """Calculate On-Balance Volume (OBV). 348 349 OBV uses volume flow to predict changes in stock price. 350 Rising OBV indicates positive volume pressure that can lead to higher prices. 351 352 Args: 353 df: DataFrame with Close and Volume columns 354 355 Returns: 356 Series with OBV values 357 """ 358 required = ["Close", "Volume"] 359 if not all(col in df.columns for col in required): 360 return pd.Series(np.nan, index=df.index, name="OBV") 361 362 # Direction: +1 if close > previous close, -1 if close < previous close, 0 if equal 363 direction = np.sign(df["Close"].diff()) 364 direction.iloc[0] = 0 # First value has no direction 365 366 # OBV is cumulative sum of signed volume 367 obv = (direction * df["Volume"]).cumsum() 368 369 return obv.rename("OBV")
Calculate On-Balance Volume (OBV).
OBV uses volume flow to predict changes in stock price. Rising OBV indicates positive volume pressure that can lead to higher prices.
Args: df: DataFrame with Close and Volume columns
Returns: Series with OBV values
372def calculate_vwap(df: pd.DataFrame) -> pd.Series: 373 """Calculate Volume Weighted Average Price (VWAP). 374 375 VWAP gives the average price weighted by volume. 376 It's often used as a trading benchmark. 377 378 Args: 379 df: DataFrame with High, Low, Close, Volume columns 380 381 Returns: 382 Series with VWAP values 383 """ 384 required = ["High", "Low", "Close", "Volume"] 385 if not all(col in df.columns for col in required): 386 return pd.Series(np.nan, index=df.index, name="VWAP") 387 388 # Typical Price 389 typical_price = (df["High"] + df["Low"] + df["Close"]) / 3 390 391 # Cumulative TP * Volume / Cumulative Volume 392 cumulative_tp_vol = (typical_price * df["Volume"]).cumsum() 393 cumulative_vol = df["Volume"].cumsum() 394 395 vwap = cumulative_tp_vol / cumulative_vol 396 vwap = vwap.replace([np.inf, -np.inf], np.nan) 397 398 return vwap.rename("VWAP")
Calculate Volume Weighted Average Price (VWAP).
VWAP gives the average price weighted by volume. It's often used as a trading benchmark.
Args: df: DataFrame with High, Low, Close, Volume columns
Returns: Series with VWAP values
401def calculate_adx(df: pd.DataFrame, period: int = 14) -> pd.Series: 402 """Calculate Average Directional Index (ADX). 403 404 ADX measures the strength of a trend regardless of its direction. 405 - ADX > 25: Strong trend 406 - ADX < 20: Weak or no trend 407 408 Args: 409 df: DataFrame with High, Low, Close columns 410 period: Period for ADX calculation 411 412 Returns: 413 Series with ADX values 414 """ 415 required = ["High", "Low", "Close"] 416 if not all(col in df.columns for col in required): 417 return pd.Series(np.nan, index=df.index, name=f"ADX_{period}") 418 419 high = df["High"] 420 low = df["Low"] 421 close = df["Close"] 422 423 # Calculate +DM and -DM 424 plus_dm = high.diff() 425 minus_dm = -low.diff() 426 427 plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0.0) 428 minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0.0) 429 430 # True Range 431 tr1 = high - low 432 tr2 = abs(high - close.shift(1)) 433 tr3 = abs(low - close.shift(1)) 434 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 435 436 # Smoothed values using Wilder's smoothing (same as TradingView) 437 atr = tr.ewm(alpha=1 / period, adjust=False).mean() 438 plus_di = 100 * (plus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr) 439 minus_di = 100 * (minus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr) 440 441 # DX and ADX 442 dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di) 443 dx = dx.replace([np.inf, -np.inf], np.nan).fillna(0) 444 445 adx = dx.ewm(alpha=1 / period, adjust=False).mean() 446 447 return adx.rename(f"ADX_{period}")
Calculate Average Directional Index (ADX).
ADX measures the strength of a trend regardless of its direction.
- ADX > 25: Strong trend
- ADX < 20: Weak or no trend
Args: df: DataFrame with High, Low, Close columns period: Period for ADX calculation
Returns: Series with ADX values
450def calculate_supertrend( 451 df: pd.DataFrame, atr_period: int = 10, multiplier: float = 3.0 452) -> pd.DataFrame: 453 """Calculate Supertrend indicator. 454 455 Supertrend is a trend-following indicator based on ATR. 456 - When price is above Supertrend line: Bullish (uptrend) 457 - When price is below Supertrend line: Bearish (downtrend) 458 459 Args: 460 df: DataFrame with High, Low, Close columns 461 atr_period: Period for ATR calculation (default: 10) 462 multiplier: ATR multiplier for bands (default: 3.0) 463 464 Returns: 465 DataFrame with columns: 466 - Supertrend: The Supertrend line value 467 - Supertrend_Direction: 1 for bullish, -1 for bearish 468 - Supertrend_Upper: Upper band 469 - Supertrend_Lower: Lower band 470 """ 471 required = ["High", "Low", "Close"] 472 if not all(col in df.columns for col in required): 473 return pd.DataFrame( 474 { 475 "Supertrend": np.nan, 476 "Supertrend_Direction": np.nan, 477 "Supertrend_Upper": np.nan, 478 "Supertrend_Lower": np.nan, 479 }, 480 index=df.index, 481 ) 482 483 high = df["High"] 484 low = df["Low"] 485 close = df["Close"] 486 487 # Calculate ATR 488 tr1 = high - low 489 tr2 = abs(high - close.shift(1)) 490 tr3 = abs(low - close.shift(1)) 491 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 492 atr = tr.ewm(alpha=1 / atr_period, adjust=False).mean() 493 494 # Calculate basic bands 495 hl2 = (high + low) / 2 496 basic_upper = hl2 + (multiplier * atr) 497 basic_lower = hl2 - (multiplier * atr) 498 499 # Initialize arrays 500 n = len(df) 501 supertrend = np.zeros(n) 502 direction = np.zeros(n) 503 final_upper = np.zeros(n) 504 final_lower = np.zeros(n) 505 506 # First value 507 final_upper[0] = basic_upper.iloc[0] 508 final_lower[0] = basic_lower.iloc[0] 509 supertrend[0] = basic_upper.iloc[0] 510 direction[0] = -1 # Start bearish 511 512 # Calculate Supertrend 513 for i in range(1, n): 514 # Final Upper Band 515 if basic_upper.iloc[i] < final_upper[i - 1] or close.iloc[i - 1] > final_upper[i - 1]: 516 final_upper[i] = basic_upper.iloc[i] 517 else: 518 final_upper[i] = final_upper[i - 1] 519 520 # Final Lower Band 521 if basic_lower.iloc[i] > final_lower[i - 1] or close.iloc[i - 1] < final_lower[i - 1]: 522 final_lower[i] = basic_lower.iloc[i] 523 else: 524 final_lower[i] = final_lower[i - 1] 525 526 # Supertrend and Direction 527 if supertrend[i - 1] == final_upper[i - 1]: 528 # Was bearish 529 if close.iloc[i] > final_upper[i]: 530 supertrend[i] = final_lower[i] 531 direction[i] = 1 # Bullish 532 else: 533 supertrend[i] = final_upper[i] 534 direction[i] = -1 # Bearish 535 else: 536 # Was bullish 537 if close.iloc[i] < final_lower[i]: 538 supertrend[i] = final_upper[i] 539 direction[i] = -1 # Bearish 540 else: 541 supertrend[i] = final_lower[i] 542 direction[i] = 1 # Bullish 543 544 return pd.DataFrame( 545 { 546 "Supertrend": supertrend, 547 "Supertrend_Direction": direction, 548 "Supertrend_Upper": final_upper, 549 "Supertrend_Lower": final_lower, 550 }, 551 index=df.index, 552 )
Calculate Supertrend indicator.
Supertrend is a trend-following indicator based on ATR.
- When price is above Supertrend line: Bullish (uptrend)
- When price is below Supertrend line: Bearish (downtrend)
Args: df: DataFrame with High, Low, Close columns atr_period: Period for ATR calculation (default: 10) multiplier: ATR multiplier for bands (default: 3.0)
Returns: DataFrame with columns: - Supertrend: The Supertrend line value - Supertrend_Direction: 1 for bullish, -1 for bearish - Supertrend_Upper: Upper band - Supertrend_Lower: Lower band
93def calculate_tilson_t3( 94 df: pd.DataFrame, 95 period: int = 5, 96 vfactor: float = 0.7, 97 column: str = "Close", 98) -> pd.Series: 99 """Calculate Tilson T3 Moving Average. 100 101 T3 is a triple-smoothed exponential moving average that reduces lag 102 while maintaining smoothness. Developed by Tim Tilson. 103 104 The T3 uses a volume factor (vfactor) to control the amount of 105 smoothing vs responsiveness: 106 - vfactor = 0: T3 behaves like a triple EMA 107 - vfactor = 1: Maximum smoothing (may overshoot) 108 - vfactor = 0.7: Tilson's recommended default 109 110 Args: 111 df: DataFrame with price data 112 period: Number of periods for EMA calculations (default 5) 113 vfactor: Volume factor for smoothing (0-1, default 0.7) 114 column: Column name to use for calculation 115 116 Returns: 117 Series with T3 values 118 119 Examples: 120 >>> t3 = calculate_tilson_t3(df, period=5, vfactor=0.7) 121 >>> # More responsive (less smooth) 122 >>> t3_fast = calculate_tilson_t3(df, period=5, vfactor=0.5) 123 >>> # More smooth (more lag) 124 >>> t3_smooth = calculate_tilson_t3(df, period=5, vfactor=0.9) 125 """ 126 col = _get_price_column(df, column) 127 if col not in df.columns: 128 return pd.Series(np.nan, index=df.index, name=f"T3_{period}") 129 130 # Calculate coefficients 131 c1 = -(vfactor**3) 132 c2 = 3 * vfactor**2 + 3 * vfactor**3 133 c3 = -6 * vfactor**2 - 3 * vfactor - 3 * vfactor**3 134 c4 = 1 + 3 * vfactor + vfactor**3 + 3 * vfactor**2 135 136 # Calculate 6 consecutive EMAs 137 ema1 = df[col].ewm(span=period, adjust=False).mean() 138 ema2 = ema1.ewm(span=period, adjust=False).mean() 139 ema3 = ema2.ewm(span=period, adjust=False).mean() 140 ema4 = ema3.ewm(span=period, adjust=False).mean() 141 ema5 = ema4.ewm(span=period, adjust=False).mean() 142 ema6 = ema5.ewm(span=period, adjust=False).mean() 143 144 # T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3 145 t3 = c1 * ema6 + c2 * ema5 + c3 * ema4 + c4 * ema3 146 147 return t3.rename(f"T3_{period}")
Calculate Tilson T3 Moving Average.
T3 is a triple-smoothed exponential moving average that reduces lag while maintaining smoothness. Developed by Tim Tilson.
The T3 uses a volume factor (vfactor) to control the amount of smoothing vs responsiveness:
- vfactor = 0: T3 behaves like a triple EMA
- vfactor = 1: Maximum smoothing (may overshoot)
- vfactor = 0.7: Tilson's recommended default
Args: df: DataFrame with price data period: Number of periods for EMA calculations (default 5) vfactor: Volume factor for smoothing (0-1, default 0.7) column: Column name to use for calculation
Returns: Series with T3 values
Examples:
t3 = calculate_tilson_t3(df, period=5, vfactor=0.7)
More responsive (less smooth)
t3_fast = calculate_tilson_t3(df, period=5, vfactor=0.5)
More smooth (more lag)
t3_smooth = calculate_tilson_t3(df, period=5, vfactor=0.9)
28def calculate_heikin_ashi(df: pd.DataFrame) -> pd.DataFrame: 29 """Calculate Heikin Ashi candlestick values. 30 31 Heikin Ashi candles smooth price data and help identify trends more clearly. 32 They use a modified formula that incorporates previous candle values. 33 34 Formulas: 35 HA_Close = (Open + High + Low + Close) / 4 36 HA_Open = (Previous_HA_Open + Previous_HA_Close) / 2 37 HA_High = max(High, HA_Open, HA_Close) 38 HA_Low = min(Low, HA_Open, HA_Close) 39 40 Note: First candle uses (Open + Close) / 2 for HA_Open since there's no previous. 41 42 Args: 43 df: DataFrame with OHLC columns (Open, High, Low, Close). 44 May also include Volume which will be preserved. 45 46 Returns: 47 DataFrame with columns: 48 - HA_Open: Heikin Ashi open price 49 - HA_High: Heikin Ashi high price 50 - HA_Low: Heikin Ashi low price 51 - HA_Close: Heikin Ashi close price 52 - Volume: Original volume (if present in input) 53 54 Examples: 55 >>> import borsapy as bp 56 >>> stock = bp.Ticker("THYAO") 57 >>> df = stock.history(period="1mo") 58 >>> ha = bp.calculate_heikin_ashi(df) 59 >>> print(ha.tail()) 60 HA_Open HA_High HA_Low HA_Close Volume 61 Date 62 2024-01-15 284.125 286.5000 283.2500 285.3750 1234567 63 2024-01-16 284.750 287.0000 284.0000 286.1250 1345678 64 65 Raises: 66 ValueError: If required OHLC columns are missing 67 68 See Also: 69 - TechnicalMixin.heikin_ashi(): Method on Ticker/Index for direct calculation 70 - TechnicalAnalyzer.heikin_ashi(): Method on TechnicalAnalyzer class 71 """ 72 # Validate required columns 73 required = ["Open", "High", "Low", "Close"] 74 missing = [col for col in required if col not in df.columns] 75 if missing: 76 raise ValueError(f"Missing required columns: {missing}") 77 78 if df.empty: 79 return pd.DataFrame(columns=["HA_Open", "HA_High", "HA_Low", "HA_Close", "Volume"]) 80 81 # Calculate HA_Close: (O + H + L + C) / 4 82 ha_close = (df["Open"] + df["High"] + df["Low"] + df["Close"]) / 4 83 84 # Calculate HA_Open iteratively (depends on previous HA values) 85 ha_open = np.zeros(len(df)) 86 87 # First candle: HA_Open = (Open + Close) / 2 88 ha_open[0] = (df["Open"].iloc[0] + df["Close"].iloc[0]) / 2 89 90 # Subsequent candles: HA_Open = (Prev_HA_Open + Prev_HA_Close) / 2 91 for i in range(1, len(df)): 92 ha_open[i] = (ha_open[i - 1] + ha_close.iloc[i - 1]) / 2 93 94 # Convert to Series with proper index 95 ha_open = pd.Series(ha_open, index=df.index) 96 97 # Calculate HA_High and HA_Low 98 ha_high = pd.concat([df["High"], ha_open, ha_close], axis=1).max(axis=1) 99 ha_low = pd.concat([df["Low"], ha_open, ha_close], axis=1).min(axis=1) 100 101 # Build result DataFrame 102 result = pd.DataFrame( 103 { 104 "HA_Open": ha_open, 105 "HA_High": ha_high, 106 "HA_Low": ha_low, 107 "HA_Close": ha_close, 108 }, 109 index=df.index, 110 ) 111 112 # Preserve Volume if present 113 if "Volume" in df.columns: 114 result["Volume"] = df["Volume"] 115 116 return result
Calculate Heikin Ashi candlestick values.
Heikin Ashi candles smooth price data and help identify trends more clearly. They use a modified formula that incorporates previous candle values.
Formulas: HA_Close = (Open + High + Low + Close) / 4 HA_Open = (Previous_HA_Open + Previous_HA_Close) / 2 HA_High = max(High, HA_Open, HA_Close) HA_Low = min(Low, HA_Open, HA_Close)
Note: First candle uses (Open + Close) / 2 for HA_Open since there's no previous.
Args: df: DataFrame with OHLC columns (Open, High, Low, Close). May also include Volume which will be preserved.
Returns: DataFrame with columns: - HA_Open: Heikin Ashi open price - HA_High: Heikin Ashi high price - HA_Low: Heikin Ashi low price - HA_Close: Heikin Ashi close price - Volume: Original volume (if present in input)
Examples:
import borsapy as bp stock = bp.Ticker("THYAO") df = stock.history(period="1mo") ha = bp.calculate_heikin_ashi(df) print(ha.tail()) HA_Open HA_High HA_Low HA_Close Volume Date 2024-01-15 284.125 286.5000 283.2500 285.3750 1234567 2024-01-16 284.750 287.0000 284.0000 286.1250 1345678
Raises: ValueError: If required OHLC columns are missing
See Also: - TechnicalMixin.heikin_ashi(): Method on Ticker/Index for direct calculation - TechnicalAnalyzer.heikin_ashi(): Method on TechnicalAnalyzer class
413def create_replay( 414 symbol: str, 415 period: str = "1y", 416 interval: str = "1d", 417 speed: float = 1.0, 418 realtime_injection: bool = False, 419) -> ReplaySession: 420 """ 421 Create a ReplaySession with historical data loaded automatically. 422 423 Convenience function that loads historical data from TradingView 424 and creates a ReplaySession. 425 426 Args: 427 symbol: Stock symbol (e.g., "THYAO", "GARAN") 428 period: Historical period to load. 429 Valid values: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max 430 interval: Candle interval. 431 Valid values: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo 432 speed: Playback speed multiplier. 433 1.0 = real-time 434 10.0 = 10x speed 435 0.0 = no delay (as fast as possible) 436 realtime_injection: If True, delays between candles are based 437 on actual time intervals. 438 439 Returns: 440 ReplaySession with loaded data. 441 442 Raises: 443 ValueError: If symbol not found or no data available. 444 445 Example: 446 >>> session = bp.create_replay("THYAO", period="1y", speed=100) 447 >>> for candle in session.replay(): 448 ... print(candle['close']) 449 """ 450 # Import here to avoid circular import 451 from borsapy.ticker import Ticker 452 453 # Load historical data 454 ticker = Ticker(symbol) 455 df = ticker.history(period=period, interval=interval) 456 457 if df is None or len(df) == 0: 458 raise ValueError(f"No historical data available for {symbol}") 459 460 # Create session with loaded data 461 session = ReplaySession( 462 symbol=symbol, 463 df=df, 464 speed=speed, 465 realtime_injection=realtime_injection, 466 ) 467 468 return session
Create a ReplaySession with historical data loaded automatically.
Convenience function that loads historical data from TradingView and creates a ReplaySession.
Args: symbol: Stock symbol (e.g., "THYAO", "GARAN") period: Historical period to load. Valid values: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max interval: Candle interval. Valid values: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo speed: Playback speed multiplier. 1.0 = real-time 10.0 = 10x speed 0.0 = no delay (as fast as possible) realtime_injection: If True, delays between candles are based on actual time intervals.
Returns: ReplaySession with loaded data.
Raises: ValueError: If symbol not found or no data available.
Example:
session = bp.create_replay("THYAO", period="1y", speed=100) for candle in session.replay(): ... print(candle['close'])
Base exception for all borsapy errors.
11class TickerNotFoundError(BorsapyError): 12 """Raised when a ticker symbol is not found.""" 13 14 def __init__(self, symbol: str): 15 self.symbol = symbol 16 super().__init__(f"Ticker not found: {symbol}")
Raised when a ticker symbol is not found.
19class DataNotAvailableError(BorsapyError): 20 """Raised when requested data is not available.""" 21 22 def __init__(self, message: str = "Data not available"): 23 super().__init__(message)
Raised when requested data is not available.
26class APIError(BorsapyError): 27 """Raised when an API request fails.""" 28 29 def __init__(self, message: str, status_code: int | None = None): 30 self.status_code = status_code 31 super().__init__( 32 f"API Error: {message}" + (f" (status: {status_code})" if status_code else "") 33 )
Raised when an API request fails.
Raised when authentication fails.
Raised when rate limit is exceeded.
48class InvalidPeriodError(BorsapyError): 49 """Raised when an invalid period is specified.""" 50 51 def __init__(self, period: str): 52 self.period = period 53 valid_periods = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"] 54 super().__init__(f"Invalid period: {period}. Valid periods: {', '.join(valid_periods)}")
Raised when an invalid period is specified.
57class InvalidIntervalError(BorsapyError): 58 """Raised when an invalid interval is specified.""" 59 60 def __init__(self, interval: str): 61 self.interval = interval 62 valid_intervals = ["1m", "5m", "15m", "30m", "1h", "1d", "1wk", "1mo"] 63 super().__init__( 64 f"Invalid interval: {interval}. Valid intervals: {', '.join(valid_intervals)}" 65 )
Raised when an invalid interval is specified.
22def set_tradingview_auth( 23 username: str | None = None, 24 password: str | None = None, 25 session: str | None = None, 26 session_sign: str | None = None, 27) -> dict: 28 """ 29 Set TradingView authentication credentials for real-time data access. 30 31 You can authenticate in two ways: 32 1. Username/password: Will perform login and get session tokens 33 2. Session tokens: Use existing sessionid and sessionid_sign cookies 34 35 Args: 36 username: TradingView username or email 37 password: TradingView password 38 session: Existing sessionid cookie value 39 session_sign: Existing sessionid_sign cookie value 40 41 Returns: 42 Dict with user info and session details 43 44 Examples: 45 >>> import borsapy as bp 46 >>> # Login with credentials 47 >>> bp.set_tradingview_auth(username="user@email.com", password="mypassword") 48 >>> # Or use existing session 49 >>> bp.set_tradingview_auth(session="abc123", session_sign="xyz789") 50 >>> # Now get real-time data 51 >>> stock = bp.Ticker("THYAO") 52 >>> stock.info["last"] # Real-time price (no 15min delay) 53 """ 54 global _auth_credentials 55 56 provider = get_tradingview_provider() 57 58 if username and password: 59 # Login with credentials 60 user_info = provider.login_user(username, password) 61 _auth_credentials = { 62 "session": user_info["session"], 63 "session_sign": user_info["session_sign"], 64 "auth_token": user_info.get("auth_token"), 65 "user": user_info, 66 } 67 elif session: 68 # Use existing session tokens 69 user_info = provider.get_user(session, session_sign or "") 70 _auth_credentials = { 71 "session": session, 72 "session_sign": session_sign or "", 73 "auth_token": user_info.get("auth_token"), 74 "user": user_info, 75 } 76 else: 77 raise ValueError("Provide either username/password or session/session_sign") 78 79 return _auth_credentials
Set TradingView authentication credentials for real-time data access.
You can authenticate in two ways:
- Username/password: Will perform login and get session tokens
- Session tokens: Use existing sessionid and sessionid_sign cookies
Args: username: TradingView username or email password: TradingView password session: Existing sessionid cookie value session_sign: Existing sessionid_sign cookie value
Returns: Dict with user info and session details
Examples:
import borsapy as bp
Login with credentials
bp.set_tradingview_auth(username="user@email.com", password="mypassword")
Or use existing session
bp.set_tradingview_auth(session="abc123", session_sign="xyz789")
Now get real-time data
stock = bp.Ticker("THYAO") stock.info["last"] # Real-time price (no 15min delay)
88def get_tradingview_auth() -> dict | None: 89 """Get current TradingView authentication credentials.""" 90 return _auth_credentials
Get current TradingView authentication credentials.
82def clear_tradingview_auth() -> None: 83 """Clear TradingView authentication credentials.""" 84 global _auth_credentials 85 _auth_credentials = None
Clear TradingView authentication credentials.
2144def create_stream(auth_token: str | None = None) -> TradingViewStream: 2145 """ 2146 Create and return a TradingViewStream instance. 2147 2148 Args: 2149 auth_token: Optional auth token for real-time data. 2150 2151 Returns: 2152 TradingViewStream instance (not connected). 2153 2154 Example: 2155 >>> stream = bp.create_stream() 2156 >>> stream.connect() 2157 """ 2158 return TradingViewStream(auth_token=auth_token)
Create and return a TradingViewStream instance.
Args: auth_token: Optional auth token for real-time data.
Returns: TradingViewStream instance (not connected).
Example:
stream = bp.create_stream() stream.connect()
484class Backtest: 485 """ 486 Backtest engine for evaluating trading strategies. 487 488 Runs a strategy function over historical data and calculates 489 comprehensive performance metrics. 490 491 Attributes: 492 symbol: Stock symbol to backtest. 493 strategy: Strategy function to evaluate. 494 period: Historical data period. 495 interval: Data interval (e.g., "1d", "1h"). 496 capital: Initial capital. 497 commission: Commission rate per trade (e.g., 0.001 = 0.1%). 498 indicators: List of indicators to calculate. 499 500 Examples: 501 >>> def my_strategy(candle, position, indicators): 502 ... if indicators['rsi'] < 30: 503 ... return 'BUY' 504 ... elif indicators['rsi'] > 70: 505 ... return 'SELL' 506 ... return 'HOLD' 507 508 >>> bt = Backtest("THYAO", my_strategy, period="1y") 509 >>> result = bt.run() 510 >>> print(result.sharpe_ratio) 511 """ 512 513 # Indicator period warmup 514 WARMUP_PERIOD = 50 515 516 def __init__( 517 self, 518 symbol: str, 519 strategy: StrategyFunc, 520 period: str = "1y", 521 interval: str = "1d", 522 capital: float = 100_000.0, 523 commission: float = 0.001, 524 indicators: list[str] | None = None, 525 slippage: float = 0.0, # Future use 526 ): 527 """ 528 Initialize Backtest. 529 530 Args: 531 symbol: Stock symbol (e.g., "THYAO"). 532 strategy: Strategy function with signature: 533 strategy(candle, position, indicators) -> 'BUY'|'SELL'|'HOLD'|None 534 period: Historical data period (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y). 535 interval: Data interval (1m, 5m, 15m, 30m, 1h, 4h, 1d). 536 capital: Initial capital in TL. 537 commission: Commission rate per trade (0.001 = 0.1%). 538 indicators: List of indicators to calculate. Options: 539 'rsi', 'rsi_7', 'sma_20', 'sma_50', 'sma_200', 540 'ema_12', 'ema_26', 'ema_50', 'macd', 'bollinger', 541 'atr', 'atr_20', 'stochastic', 'adx' 542 slippage: Slippage per trade (for future use). 543 """ 544 self.symbol = symbol.upper() 545 self.strategy = strategy 546 self.period = period 547 self.interval = interval 548 self.capital = capital 549 self.commission = commission 550 self.indicators = indicators or ["rsi", "sma_20", "ema_12", "macd"] 551 self.slippage = slippage 552 553 # Strategy name for reporting 554 self._strategy_name = getattr(strategy, "__name__", "custom_strategy") 555 556 # Data storage 557 self._df: pd.DataFrame | None = None 558 self._df_with_indicators: pd.DataFrame | None = None 559 560 def _load_data(self) -> pd.DataFrame: 561 """Load historical data from Ticker.""" 562 from borsapy.ticker import Ticker 563 564 ticker = Ticker(self.symbol) 565 df = ticker.history(period=self.period, interval=self.interval) 566 567 if df is None or df.empty: 568 raise ValueError(f"No historical data available for {self.symbol}") 569 570 return df 571 572 def _calculate_indicators(self, df: pd.DataFrame) -> pd.DataFrame: 573 """Add indicator columns to DataFrame.""" 574 from borsapy.technical import ( 575 calculate_adx, 576 calculate_atr, 577 calculate_bollinger_bands, 578 calculate_ema, 579 calculate_macd, 580 calculate_rsi, 581 calculate_sma, 582 calculate_stochastic, 583 ) 584 585 result = df.copy() 586 587 for ind in self.indicators: 588 ind_lower = ind.lower() 589 590 # RSI variants 591 if ind_lower == "rsi": 592 result["rsi"] = calculate_rsi(df, period=14) 593 elif ind_lower.startswith("rsi_"): 594 try: 595 period = int(ind_lower.split("_")[1]) 596 result[f"rsi_{period}"] = calculate_rsi(df, period=period) 597 except (IndexError, ValueError): 598 pass 599 600 # SMA variants 601 elif ind_lower.startswith("sma_"): 602 try: 603 period = int(ind_lower.split("_")[1]) 604 result[f"sma_{period}"] = calculate_sma(df, period=period) 605 except (IndexError, ValueError): 606 pass 607 608 # EMA variants 609 elif ind_lower.startswith("ema_"): 610 try: 611 period = int(ind_lower.split("_")[1]) 612 result[f"ema_{period}"] = calculate_ema(df, period=period) 613 except (IndexError, ValueError): 614 pass 615 616 # MACD 617 elif ind_lower == "macd": 618 macd_df = calculate_macd(df) 619 result["macd"] = macd_df["MACD"] 620 result["macd_signal"] = macd_df["Signal"] 621 result["macd_histogram"] = macd_df["Histogram"] 622 623 # Bollinger Bands 624 elif ind_lower in ("bollinger", "bb"): 625 bb_df = calculate_bollinger_bands(df) 626 result["bb_upper"] = bb_df["BB_Upper"] 627 result["bb_middle"] = bb_df["BB_Middle"] 628 result["bb_lower"] = bb_df["BB_Lower"] 629 630 # ATR variants 631 elif ind_lower == "atr": 632 result["atr"] = calculate_atr(df, period=14) 633 elif ind_lower.startswith("atr_"): 634 try: 635 period = int(ind_lower.split("_")[1]) 636 result[f"atr_{period}"] = calculate_atr(df, period=period) 637 except (IndexError, ValueError): 638 pass 639 640 # Stochastic 641 elif ind_lower in ("stochastic", "stoch"): 642 stoch_df = calculate_stochastic(df) 643 result["stoch_k"] = stoch_df["Stoch_K"] 644 result["stoch_d"] = stoch_df["Stoch_D"] 645 646 # ADX 647 elif ind_lower == "adx": 648 result["adx"] = calculate_adx(df, period=14) 649 650 return result 651 652 def _get_indicators_at(self, idx: int) -> dict[str, float]: 653 """Get indicator values at specific index.""" 654 if self._df_with_indicators is None: 655 return {} 656 657 row = self._df_with_indicators.iloc[idx] 658 indicators = {} 659 660 # Extract all non-OHLCV columns as indicators 661 exclude_cols = {"Open", "High", "Low", "Close", "Volume", "Adj Close"} 662 663 for col in self._df_with_indicators.columns: 664 if col not in exclude_cols: 665 val = row[col] 666 if pd.notna(val): 667 indicators[col] = float(val) 668 669 return indicators 670 671 def _build_candle(self, idx: int) -> dict[str, Any]: 672 """Build candle dict from DataFrame row.""" 673 if self._df is None: 674 return {} 675 676 row = self._df.iloc[idx] 677 timestamp = self._df.index[idx] 678 679 if isinstance(timestamp, pd.Timestamp): 680 timestamp = timestamp.to_pydatetime() 681 682 return { 683 "timestamp": timestamp, 684 "open": float(row["Open"]), 685 "high": float(row["High"]), 686 "low": float(row["Low"]), 687 "close": float(row["Close"]), 688 "volume": float(row.get("Volume", 0)) if "Volume" in row else 0, 689 "_index": idx, 690 } 691 692 def run(self) -> BacktestResult: 693 """ 694 Run the backtest. 695 696 Returns: 697 BacktestResult with all performance metrics. 698 699 Raises: 700 ValueError: If no data available for symbol. 701 """ 702 # Load data 703 self._df = self._load_data() 704 self._df_with_indicators = self._calculate_indicators(self._df) 705 706 # Initialize state 707 cash = self.capital 708 position: Position = None 709 shares = 0.0 710 trades: list[Trade] = [] 711 current_trade: Trade | None = None 712 713 # Track equity curve 714 equity_values = [] 715 dates = [] 716 717 # Buy & hold tracking 718 initial_price = self._df["Close"].iloc[self.WARMUP_PERIOD] 719 bh_shares = self.capital / initial_price 720 721 # Run simulation 722 for idx in range(self.WARMUP_PERIOD, len(self._df)): 723 candle = self._build_candle(idx) 724 indicators = self._get_indicators_at(idx) 725 price = candle["close"] 726 timestamp = candle["timestamp"] 727 728 # Get strategy signal 729 try: 730 signal = self.strategy(candle, position, indicators) 731 except Exception: 732 signal = "HOLD" 733 734 # Execute trades 735 if signal == "BUY" and position is None: 736 # Calculate shares to buy (use all available cash) 737 entry_commission = cash * self.commission 738 available = cash - entry_commission 739 shares = available / price 740 741 current_trade = Trade( 742 entry_time=timestamp, 743 entry_price=price, 744 side="long", 745 shares=shares, 746 commission=entry_commission, 747 ) 748 749 cash = 0.0 750 position = "long" 751 752 elif signal == "SELL" and position == "long" and current_trade is not None: 753 # Close position 754 exit_value = shares * price 755 exit_commission = exit_value * self.commission 756 757 current_trade.exit_time = timestamp 758 current_trade.exit_price = price 759 current_trade.commission += exit_commission 760 761 trades.append(current_trade) 762 763 cash = exit_value - exit_commission 764 shares = 0.0 765 position = None 766 current_trade = None 767 768 # Track equity 769 if position == "long": 770 equity = shares * price 771 else: 772 equity = cash 773 774 equity_values.append(equity) 775 dates.append(timestamp) 776 777 # Close any open position at end 778 if position == "long" and current_trade is not None: 779 final_price = self._df["Close"].iloc[-1] 780 exit_value = shares * final_price 781 exit_commission = exit_value * self.commission 782 783 current_trade.exit_time = self._df.index[-1] 784 if isinstance(current_trade.exit_time, pd.Timestamp): 785 current_trade.exit_time = current_trade.exit_time.to_pydatetime() 786 current_trade.exit_price = final_price 787 current_trade.commission += exit_commission 788 789 trades.append(current_trade) 790 791 # Build curves 792 equity_curve = pd.Series(equity_values, index=pd.DatetimeIndex(dates)) 793 794 # Calculate drawdown curve 795 running_max = equity_curve.cummax() 796 drawdown_curve = (equity_curve - running_max) / running_max 797 798 # Buy & hold curve 799 bh_values = self._df["Close"].iloc[self.WARMUP_PERIOD:] * bh_shares 800 buy_hold_curve = pd.Series(bh_values.values, index=pd.DatetimeIndex(dates)) 801 802 return BacktestResult( 803 symbol=self.symbol, 804 period=self.period, 805 interval=self.interval, 806 strategy_name=self._strategy_name, 807 initial_capital=self.capital, 808 commission=self.commission, 809 trades=trades, 810 equity_curve=equity_curve, 811 drawdown_curve=drawdown_curve, 812 buy_hold_curve=buy_hold_curve, 813 )
Backtest engine for evaluating trading strategies.
Runs a strategy function over historical data and calculates comprehensive performance metrics.
Attributes: symbol: Stock symbol to backtest. strategy: Strategy function to evaluate. period: Historical data period. interval: Data interval (e.g., "1d", "1h"). capital: Initial capital. commission: Commission rate per trade (e.g., 0.001 = 0.1%). indicators: List of indicators to calculate.
Examples:
def my_strategy(candle, position, indicators): ... if indicators['rsi'] < 30: ... return 'BUY' ... elif indicators['rsi'] > 70: ... return 'SELL' ... return 'HOLD'
>>> bt = Backtest("THYAO", my_strategy, period="1y") >>> result = bt.run() >>> print(result.sharpe_ratio)
516 def __init__( 517 self, 518 symbol: str, 519 strategy: StrategyFunc, 520 period: str = "1y", 521 interval: str = "1d", 522 capital: float = 100_000.0, 523 commission: float = 0.001, 524 indicators: list[str] | None = None, 525 slippage: float = 0.0, # Future use 526 ): 527 """ 528 Initialize Backtest. 529 530 Args: 531 symbol: Stock symbol (e.g., "THYAO"). 532 strategy: Strategy function with signature: 533 strategy(candle, position, indicators) -> 'BUY'|'SELL'|'HOLD'|None 534 period: Historical data period (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y). 535 interval: Data interval (1m, 5m, 15m, 30m, 1h, 4h, 1d). 536 capital: Initial capital in TL. 537 commission: Commission rate per trade (0.001 = 0.1%). 538 indicators: List of indicators to calculate. Options: 539 'rsi', 'rsi_7', 'sma_20', 'sma_50', 'sma_200', 540 'ema_12', 'ema_26', 'ema_50', 'macd', 'bollinger', 541 'atr', 'atr_20', 'stochastic', 'adx' 542 slippage: Slippage per trade (for future use). 543 """ 544 self.symbol = symbol.upper() 545 self.strategy = strategy 546 self.period = period 547 self.interval = interval 548 self.capital = capital 549 self.commission = commission 550 self.indicators = indicators or ["rsi", "sma_20", "ema_12", "macd"] 551 self.slippage = slippage 552 553 # Strategy name for reporting 554 self._strategy_name = getattr(strategy, "__name__", "custom_strategy") 555 556 # Data storage 557 self._df: pd.DataFrame | None = None 558 self._df_with_indicators: pd.DataFrame | None = None
Initialize Backtest.
Args: symbol: Stock symbol (e.g., "THYAO"). strategy: Strategy function with signature: strategy(candle, position, indicators) -> 'BUY'|'SELL'|'HOLD'|None period: Historical data period (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y). interval: Data interval (1m, 5m, 15m, 30m, 1h, 4h, 1d). capital: Initial capital in TL. commission: Commission rate per trade (0.001 = 0.1%). indicators: List of indicators to calculate. Options: 'rsi', 'rsi_7', 'sma_20', 'sma_50', 'sma_200', 'ema_12', 'ema_26', 'ema_50', 'macd', 'bollinger', 'atr', 'atr_20', 'stochastic', 'adx' slippage: Slippage per trade (for future use).
692 def run(self) -> BacktestResult: 693 """ 694 Run the backtest. 695 696 Returns: 697 BacktestResult with all performance metrics. 698 699 Raises: 700 ValueError: If no data available for symbol. 701 """ 702 # Load data 703 self._df = self._load_data() 704 self._df_with_indicators = self._calculate_indicators(self._df) 705 706 # Initialize state 707 cash = self.capital 708 position: Position = None 709 shares = 0.0 710 trades: list[Trade] = [] 711 current_trade: Trade | None = None 712 713 # Track equity curve 714 equity_values = [] 715 dates = [] 716 717 # Buy & hold tracking 718 initial_price = self._df["Close"].iloc[self.WARMUP_PERIOD] 719 bh_shares = self.capital / initial_price 720 721 # Run simulation 722 for idx in range(self.WARMUP_PERIOD, len(self._df)): 723 candle = self._build_candle(idx) 724 indicators = self._get_indicators_at(idx) 725 price = candle["close"] 726 timestamp = candle["timestamp"] 727 728 # Get strategy signal 729 try: 730 signal = self.strategy(candle, position, indicators) 731 except Exception: 732 signal = "HOLD" 733 734 # Execute trades 735 if signal == "BUY" and position is None: 736 # Calculate shares to buy (use all available cash) 737 entry_commission = cash * self.commission 738 available = cash - entry_commission 739 shares = available / price 740 741 current_trade = Trade( 742 entry_time=timestamp, 743 entry_price=price, 744 side="long", 745 shares=shares, 746 commission=entry_commission, 747 ) 748 749 cash = 0.0 750 position = "long" 751 752 elif signal == "SELL" and position == "long" and current_trade is not None: 753 # Close position 754 exit_value = shares * price 755 exit_commission = exit_value * self.commission 756 757 current_trade.exit_time = timestamp 758 current_trade.exit_price = price 759 current_trade.commission += exit_commission 760 761 trades.append(current_trade) 762 763 cash = exit_value - exit_commission 764 shares = 0.0 765 position = None 766 current_trade = None 767 768 # Track equity 769 if position == "long": 770 equity = shares * price 771 else: 772 equity = cash 773 774 equity_values.append(equity) 775 dates.append(timestamp) 776 777 # Close any open position at end 778 if position == "long" and current_trade is not None: 779 final_price = self._df["Close"].iloc[-1] 780 exit_value = shares * final_price 781 exit_commission = exit_value * self.commission 782 783 current_trade.exit_time = self._df.index[-1] 784 if isinstance(current_trade.exit_time, pd.Timestamp): 785 current_trade.exit_time = current_trade.exit_time.to_pydatetime() 786 current_trade.exit_price = final_price 787 current_trade.commission += exit_commission 788 789 trades.append(current_trade) 790 791 # Build curves 792 equity_curve = pd.Series(equity_values, index=pd.DatetimeIndex(dates)) 793 794 # Calculate drawdown curve 795 running_max = equity_curve.cummax() 796 drawdown_curve = (equity_curve - running_max) / running_max 797 798 # Buy & hold curve 799 bh_values = self._df["Close"].iloc[self.WARMUP_PERIOD:] * bh_shares 800 buy_hold_curve = pd.Series(bh_values.values, index=pd.DatetimeIndex(dates)) 801 802 return BacktestResult( 803 symbol=self.symbol, 804 period=self.period, 805 interval=self.interval, 806 strategy_name=self._strategy_name, 807 initial_capital=self.capital, 808 commission=self.commission, 809 trades=trades, 810 equity_curve=equity_curve, 811 drawdown_curve=drawdown_curve, 812 buy_hold_curve=buy_hold_curve, 813 )
Run the backtest.
Returns: BacktestResult with all performance metrics.
Raises: ValueError: If no data available for symbol.
127@dataclass 128class BacktestResult: 129 """ 130 Comprehensive backtest results with performance metrics. 131 132 Follows TradingView/Mathieu2301 result format for familiarity. 133 134 Attributes: 135 symbol: Traded symbol. 136 period: Test period (e.g., "1y"). 137 interval: Data interval (e.g., "1d"). 138 strategy_name: Name of the strategy function. 139 initial_capital: Starting capital. 140 commission: Commission rate used. 141 trades: List of executed trades. 142 equity_curve: Daily equity values. 143 drawdown_curve: Daily drawdown values. 144 buy_hold_curve: Buy & hold comparison values. 145 """ 146 147 # Identification 148 symbol: str 149 period: str 150 interval: str 151 strategy_name: str 152 153 # Configuration 154 initial_capital: float 155 commission: float 156 157 # Results 158 trades: list[Trade] = field(default_factory=list) 159 equity_curve: pd.Series = field(default_factory=lambda: pd.Series(dtype=float)) 160 drawdown_curve: pd.Series = field(default_factory=lambda: pd.Series(dtype=float)) 161 buy_hold_curve: pd.Series = field(default_factory=lambda: pd.Series(dtype=float)) 162 163 # === Performance Properties === 164 165 @property 166 def final_equity(self) -> float: 167 """Final portfolio value.""" 168 if self.equity_curve.empty: 169 return self.initial_capital 170 return float(self.equity_curve.iloc[-1]) 171 172 @property 173 def net_profit(self) -> float: 174 """Net profit in currency units.""" 175 return self.final_equity - self.initial_capital 176 177 @property 178 def net_profit_pct(self) -> float: 179 """Net profit as percentage.""" 180 if self.initial_capital == 0: 181 return 0.0 182 return (self.net_profit / self.initial_capital) * 100 183 184 @property 185 def total_trades(self) -> int: 186 """Total number of closed trades.""" 187 return len([t for t in self.trades if t.is_closed]) 188 189 @property 190 def winning_trades(self) -> int: 191 """Number of profitable trades.""" 192 return len([t for t in self.trades if t.is_closed and (t.profit or 0) > 0]) 193 194 @property 195 def losing_trades(self) -> int: 196 """Number of losing trades.""" 197 return len([t for t in self.trades if t.is_closed and (t.profit or 0) <= 0]) 198 199 @property 200 def win_rate(self) -> float: 201 """Percentage of winning trades.""" 202 if self.total_trades == 0: 203 return 0.0 204 return (self.winning_trades / self.total_trades) * 100 205 206 @property 207 def profit_factor(self) -> float: 208 """Ratio of gross profits to gross losses.""" 209 gross_profit = sum(t.profit or 0 for t in self.trades if t.is_closed and (t.profit or 0) > 0) 210 gross_loss = abs(sum(t.profit or 0 for t in self.trades if t.is_closed and (t.profit or 0) < 0)) 211 if gross_loss == 0: 212 return float("inf") if gross_profit > 0 else 0.0 213 return gross_profit / gross_loss 214 215 @property 216 def avg_trade(self) -> float: 217 """Average profit per trade.""" 218 closed = [t for t in self.trades if t.is_closed] 219 if not closed: 220 return 0.0 221 return sum(t.profit or 0 for t in closed) / len(closed) 222 223 @property 224 def avg_winning_trade(self) -> float: 225 """Average profit of winning trades.""" 226 winners = [t for t in self.trades if t.is_closed and (t.profit or 0) > 0] 227 if not winners: 228 return 0.0 229 return sum(t.profit or 0 for t in winners) / len(winners) 230 231 @property 232 def avg_losing_trade(self) -> float: 233 """Average loss of losing trades.""" 234 losers = [t for t in self.trades if t.is_closed and (t.profit or 0) < 0] 235 if not losers: 236 return 0.0 237 return sum(t.profit or 0 for t in losers) / len(losers) 238 239 @property 240 def max_consecutive_wins(self) -> int: 241 """Maximum consecutive winning trades.""" 242 return self._max_consecutive(lambda t: (t.profit or 0) > 0) 243 244 @property 245 def max_consecutive_losses(self) -> int: 246 """Maximum consecutive losing trades.""" 247 return self._max_consecutive(lambda t: (t.profit or 0) <= 0) 248 249 def _max_consecutive(self, condition: Callable[[Trade], bool]) -> int: 250 """Helper to find max consecutive trades matching condition.""" 251 closed = [t for t in self.trades if t.is_closed] 252 if not closed: 253 return 0 254 max_count = 0 255 current_count = 0 256 for trade in closed: 257 if condition(trade): 258 current_count += 1 259 max_count = max(max_count, current_count) 260 else: 261 current_count = 0 262 return max_count 263 264 @property 265 def sharpe_ratio(self) -> float: 266 """ 267 Sharpe ratio (risk-adjusted return). 268 269 Assumes 252 trading days and risk-free rate from current 10Y bond. 270 """ 271 if self.equity_curve.empty or len(self.equity_curve) < 2: 272 return float("nan") 273 274 returns = self.equity_curve.pct_change().dropna() 275 if returns.std() == 0: 276 return float("nan") 277 278 # Get risk-free rate 279 try: 280 from borsapy.bond import risk_free_rate 281 282 rf_annual = risk_free_rate() 283 except Exception: 284 rf_annual = 0.30 # Fallback 30% 285 286 rf_daily = rf_annual / 252 287 excess_returns = returns - rf_daily 288 return float(np.sqrt(252) * excess_returns.mean() / excess_returns.std()) 289 290 @property 291 def sortino_ratio(self) -> float: 292 """ 293 Sortino ratio (downside risk-adjusted return). 294 295 Uses downside deviation instead of standard deviation. 296 """ 297 if self.equity_curve.empty or len(self.equity_curve) < 2: 298 return float("nan") 299 300 returns = self.equity_curve.pct_change().dropna() 301 302 # Get risk-free rate 303 try: 304 from borsapy.bond import risk_free_rate 305 306 rf_annual = risk_free_rate() 307 except Exception: 308 rf_annual = 0.30 309 310 rf_daily = rf_annual / 252 311 excess_returns = returns - rf_daily 312 negative_returns = excess_returns[excess_returns < 0] 313 314 if len(negative_returns) == 0 or negative_returns.std() == 0: 315 return float("inf") if excess_returns.mean() > 0 else float("nan") 316 317 downside_std = negative_returns.std() 318 return float(np.sqrt(252) * excess_returns.mean() / downside_std) 319 320 @property 321 def max_drawdown(self) -> float: 322 """Maximum drawdown as percentage.""" 323 if self.drawdown_curve.empty: 324 return 0.0 325 return float(self.drawdown_curve.min()) * 100 326 327 @property 328 def max_drawdown_duration(self) -> int: 329 """Maximum drawdown duration in days.""" 330 if self.equity_curve.empty: 331 return 0 332 333 # Find periods where we're in drawdown 334 running_max = self.equity_curve.cummax() 335 in_drawdown = self.equity_curve < running_max 336 337 max_duration = 0 338 current_duration = 0 339 340 for is_dd in in_drawdown: 341 if is_dd: 342 current_duration += 1 343 max_duration = max(max_duration, current_duration) 344 else: 345 current_duration = 0 346 347 return max_duration 348 349 @property 350 def buy_hold_return(self) -> float: 351 """Buy & hold return as percentage.""" 352 if self.buy_hold_curve.empty: 353 return 0.0 354 first = self.buy_hold_curve.iloc[0] 355 last = self.buy_hold_curve.iloc[-1] 356 if first == 0: 357 return 0.0 358 return ((last - first) / first) * 100 359 360 @property 361 def vs_buy_hold(self) -> float: 362 """Strategy outperformance vs buy & hold (percentage points).""" 363 return self.net_profit_pct - self.buy_hold_return 364 365 @property 366 def calmar_ratio(self) -> float: 367 """Calmar ratio (annualized return / max drawdown).""" 368 if self.max_drawdown == 0: 369 return float("inf") if self.net_profit_pct > 0 else 0.0 370 # Annualize return (assuming 252 trading days) 371 trading_days = len(self.equity_curve) 372 if trading_days == 0: 373 return 0.0 374 annual_return = self.net_profit_pct * (252 / trading_days) 375 return annual_return / abs(self.max_drawdown) 376 377 # === Export Methods === 378 379 @property 380 def trades_df(self) -> pd.DataFrame: 381 """Get trades as DataFrame.""" 382 if not self.trades: 383 return pd.DataFrame( 384 columns=[ 385 "entry_time", 386 "entry_price", 387 "exit_time", 388 "exit_price", 389 "side", 390 "shares", 391 "commission", 392 "profit", 393 "profit_pct", 394 "duration", 395 ] 396 ) 397 return pd.DataFrame([t.to_dict() for t in self.trades]) 398 399 def to_dict(self) -> dict[str, Any]: 400 """ 401 Export results to dictionary. 402 403 Compatible with TradingView/Mathieu2301 format. 404 """ 405 return { 406 # Identification 407 "symbol": self.symbol, 408 "period": self.period, 409 "interval": self.interval, 410 "strategy_name": self.strategy_name, 411 # Configuration 412 "initial_capital": self.initial_capital, 413 "commission": self.commission, 414 # Summary 415 "net_profit": round(self.net_profit, 2), 416 "net_profit_pct": round(self.net_profit_pct, 2), 417 "final_equity": round(self.final_equity, 2), 418 # Trade Statistics 419 "total_trades": self.total_trades, 420 "winning_trades": self.winning_trades, 421 "losing_trades": self.losing_trades, 422 "win_rate": round(self.win_rate, 2), 423 "profit_factor": round(self.profit_factor, 2) if self.profit_factor != float("inf") else "inf", 424 "avg_trade": round(self.avg_trade, 2), 425 "avg_winning_trade": round(self.avg_winning_trade, 2), 426 "avg_losing_trade": round(self.avg_losing_trade, 2), 427 "max_consecutive_wins": self.max_consecutive_wins, 428 "max_consecutive_losses": self.max_consecutive_losses, 429 # Risk Metrics 430 "sharpe_ratio": round(self.sharpe_ratio, 2) if not np.isnan(self.sharpe_ratio) else None, 431 "sortino_ratio": round(self.sortino_ratio, 2) if not np.isnan(self.sortino_ratio) and self.sortino_ratio != float("inf") else None, 432 "calmar_ratio": round(self.calmar_ratio, 2) if self.calmar_ratio != float("inf") else None, 433 "max_drawdown": round(self.max_drawdown, 2), 434 "max_drawdown_duration": self.max_drawdown_duration, 435 # Comparison 436 "buy_hold_return": round(self.buy_hold_return, 2), 437 "vs_buy_hold": round(self.vs_buy_hold, 2), 438 } 439 440 def summary(self) -> str: 441 """ 442 Generate human-readable performance summary. 443 444 Returns: 445 Formatted summary string. 446 """ 447 d = self.to_dict() 448 449 lines = [ 450 "=" * 60, 451 f"BACKTEST RESULTS: {d['symbol']} ({d['strategy_name']})", 452 "=" * 60, 453 f"Period: {d['period']} | Interval: {d['interval']}", 454 f"Initial Capital: {d['initial_capital']:,.2f} TL", 455 f"Commission: {d['commission']*100:.2f}%", 456 "", 457 "--- PERFORMANCE ---", 458 f"Net Profit: {d['net_profit']:,.2f} TL ({d['net_profit_pct']:+.2f}%)", 459 f"Final Equity: {d['final_equity']:,.2f} TL", 460 f"Buy & Hold: {d['buy_hold_return']:+.2f}%", 461 f"vs B&H: {d['vs_buy_hold']:+.2f}%", 462 "", 463 "--- TRADE STATISTICS ---", 464 f"Total Trades: {d['total_trades']}", 465 f"Winning: {d['winning_trades']} | Losing: {d['losing_trades']}", 466 f"Win Rate: {d['win_rate']:.1f}%", 467 f"Profit Factor: {d['profit_factor']}", 468 f"Avg Trade: {d['avg_trade']:,.2f} TL", 469 f"Avg Winner: {d['avg_winning_trade']:,.2f} TL | Avg Loser: {d['avg_losing_trade']:,.2f} TL", 470 f"Max Consecutive Wins: {d['max_consecutive_wins']} | Losses: {d['max_consecutive_losses']}", 471 "", 472 "--- RISK METRICS ---", 473 f"Sharpe Ratio: {d['sharpe_ratio'] if d['sharpe_ratio'] else 'N/A'}", 474 f"Sortino Ratio: {d['sortino_ratio'] if d['sortino_ratio'] else 'N/A'}", 475 f"Calmar Ratio: {d['calmar_ratio'] if d['calmar_ratio'] else 'N/A'}", 476 f"Max Drawdown: {d['max_drawdown']:.2f}%", 477 f"Max DD Duration: {d['max_drawdown_duration']} days", 478 "=" * 60, 479 ] 480 481 return "\n".join(lines)
Comprehensive backtest results with performance metrics.
Follows TradingView/Mathieu2301 result format for familiarity.
Attributes: symbol: Traded symbol. period: Test period (e.g., "1y"). interval: Data interval (e.g., "1d"). strategy_name: Name of the strategy function. initial_capital: Starting capital. commission: Commission rate used. trades: List of executed trades. equity_curve: Daily equity values. drawdown_curve: Daily drawdown values. buy_hold_curve: Buy & hold comparison values.
165 @property 166 def final_equity(self) -> float: 167 """Final portfolio value.""" 168 if self.equity_curve.empty: 169 return self.initial_capital 170 return float(self.equity_curve.iloc[-1])
Final portfolio value.
172 @property 173 def net_profit(self) -> float: 174 """Net profit in currency units.""" 175 return self.final_equity - self.initial_capital
Net profit in currency units.
177 @property 178 def net_profit_pct(self) -> float: 179 """Net profit as percentage.""" 180 if self.initial_capital == 0: 181 return 0.0 182 return (self.net_profit / self.initial_capital) * 100
Net profit as percentage.
184 @property 185 def total_trades(self) -> int: 186 """Total number of closed trades.""" 187 return len([t for t in self.trades if t.is_closed])
Total number of closed trades.
189 @property 190 def winning_trades(self) -> int: 191 """Number of profitable trades.""" 192 return len([t for t in self.trades if t.is_closed and (t.profit or 0) > 0])
Number of profitable trades.
194 @property 195 def losing_trades(self) -> int: 196 """Number of losing trades.""" 197 return len([t for t in self.trades if t.is_closed and (t.profit or 0) <= 0])
Number of losing trades.
199 @property 200 def win_rate(self) -> float: 201 """Percentage of winning trades.""" 202 if self.total_trades == 0: 203 return 0.0 204 return (self.winning_trades / self.total_trades) * 100
Percentage of winning trades.
206 @property 207 def profit_factor(self) -> float: 208 """Ratio of gross profits to gross losses.""" 209 gross_profit = sum(t.profit or 0 for t in self.trades if t.is_closed and (t.profit or 0) > 0) 210 gross_loss = abs(sum(t.profit or 0 for t in self.trades if t.is_closed and (t.profit or 0) < 0)) 211 if gross_loss == 0: 212 return float("inf") if gross_profit > 0 else 0.0 213 return gross_profit / gross_loss
Ratio of gross profits to gross losses.
215 @property 216 def avg_trade(self) -> float: 217 """Average profit per trade.""" 218 closed = [t for t in self.trades if t.is_closed] 219 if not closed: 220 return 0.0 221 return sum(t.profit or 0 for t in closed) / len(closed)
Average profit per trade.
223 @property 224 def avg_winning_trade(self) -> float: 225 """Average profit of winning trades.""" 226 winners = [t for t in self.trades if t.is_closed and (t.profit or 0) > 0] 227 if not winners: 228 return 0.0 229 return sum(t.profit or 0 for t in winners) / len(winners)
Average profit of winning trades.
231 @property 232 def avg_losing_trade(self) -> float: 233 """Average loss of losing trades.""" 234 losers = [t for t in self.trades if t.is_closed and (t.profit or 0) < 0] 235 if not losers: 236 return 0.0 237 return sum(t.profit or 0 for t in losers) / len(losers)
Average loss of losing trades.
239 @property 240 def max_consecutive_wins(self) -> int: 241 """Maximum consecutive winning trades.""" 242 return self._max_consecutive(lambda t: (t.profit or 0) > 0)
Maximum consecutive winning trades.
244 @property 245 def max_consecutive_losses(self) -> int: 246 """Maximum consecutive losing trades.""" 247 return self._max_consecutive(lambda t: (t.profit or 0) <= 0)
Maximum consecutive losing trades.
264 @property 265 def sharpe_ratio(self) -> float: 266 """ 267 Sharpe ratio (risk-adjusted return). 268 269 Assumes 252 trading days and risk-free rate from current 10Y bond. 270 """ 271 if self.equity_curve.empty or len(self.equity_curve) < 2: 272 return float("nan") 273 274 returns = self.equity_curve.pct_change().dropna() 275 if returns.std() == 0: 276 return float("nan") 277 278 # Get risk-free rate 279 try: 280 from borsapy.bond import risk_free_rate 281 282 rf_annual = risk_free_rate() 283 except Exception: 284 rf_annual = 0.30 # Fallback 30% 285 286 rf_daily = rf_annual / 252 287 excess_returns = returns - rf_daily 288 return float(np.sqrt(252) * excess_returns.mean() / excess_returns.std())
Sharpe ratio (risk-adjusted return).
Assumes 252 trading days and risk-free rate from current 10Y bond.
290 @property 291 def sortino_ratio(self) -> float: 292 """ 293 Sortino ratio (downside risk-adjusted return). 294 295 Uses downside deviation instead of standard deviation. 296 """ 297 if self.equity_curve.empty or len(self.equity_curve) < 2: 298 return float("nan") 299 300 returns = self.equity_curve.pct_change().dropna() 301 302 # Get risk-free rate 303 try: 304 from borsapy.bond import risk_free_rate 305 306 rf_annual = risk_free_rate() 307 except Exception: 308 rf_annual = 0.30 309 310 rf_daily = rf_annual / 252 311 excess_returns = returns - rf_daily 312 negative_returns = excess_returns[excess_returns < 0] 313 314 if len(negative_returns) == 0 or negative_returns.std() == 0: 315 return float("inf") if excess_returns.mean() > 0 else float("nan") 316 317 downside_std = negative_returns.std() 318 return float(np.sqrt(252) * excess_returns.mean() / downside_std)
Sortino ratio (downside risk-adjusted return).
Uses downside deviation instead of standard deviation.
320 @property 321 def max_drawdown(self) -> float: 322 """Maximum drawdown as percentage.""" 323 if self.drawdown_curve.empty: 324 return 0.0 325 return float(self.drawdown_curve.min()) * 100
Maximum drawdown as percentage.
327 @property 328 def max_drawdown_duration(self) -> int: 329 """Maximum drawdown duration in days.""" 330 if self.equity_curve.empty: 331 return 0 332 333 # Find periods where we're in drawdown 334 running_max = self.equity_curve.cummax() 335 in_drawdown = self.equity_curve < running_max 336 337 max_duration = 0 338 current_duration = 0 339 340 for is_dd in in_drawdown: 341 if is_dd: 342 current_duration += 1 343 max_duration = max(max_duration, current_duration) 344 else: 345 current_duration = 0 346 347 return max_duration
Maximum drawdown duration in days.
349 @property 350 def buy_hold_return(self) -> float: 351 """Buy & hold return as percentage.""" 352 if self.buy_hold_curve.empty: 353 return 0.0 354 first = self.buy_hold_curve.iloc[0] 355 last = self.buy_hold_curve.iloc[-1] 356 if first == 0: 357 return 0.0 358 return ((last - first) / first) * 100
Buy & hold return as percentage.
360 @property 361 def vs_buy_hold(self) -> float: 362 """Strategy outperformance vs buy & hold (percentage points).""" 363 return self.net_profit_pct - self.buy_hold_return
Strategy outperformance vs buy & hold (percentage points).
365 @property 366 def calmar_ratio(self) -> float: 367 """Calmar ratio (annualized return / max drawdown).""" 368 if self.max_drawdown == 0: 369 return float("inf") if self.net_profit_pct > 0 else 0.0 370 # Annualize return (assuming 252 trading days) 371 trading_days = len(self.equity_curve) 372 if trading_days == 0: 373 return 0.0 374 annual_return = self.net_profit_pct * (252 / trading_days) 375 return annual_return / abs(self.max_drawdown)
Calmar ratio (annualized return / max drawdown).
379 @property 380 def trades_df(self) -> pd.DataFrame: 381 """Get trades as DataFrame.""" 382 if not self.trades: 383 return pd.DataFrame( 384 columns=[ 385 "entry_time", 386 "entry_price", 387 "exit_time", 388 "exit_price", 389 "side", 390 "shares", 391 "commission", 392 "profit", 393 "profit_pct", 394 "duration", 395 ] 396 ) 397 return pd.DataFrame([t.to_dict() for t in self.trades])
Get trades as DataFrame.
399 def to_dict(self) -> dict[str, Any]: 400 """ 401 Export results to dictionary. 402 403 Compatible with TradingView/Mathieu2301 format. 404 """ 405 return { 406 # Identification 407 "symbol": self.symbol, 408 "period": self.period, 409 "interval": self.interval, 410 "strategy_name": self.strategy_name, 411 # Configuration 412 "initial_capital": self.initial_capital, 413 "commission": self.commission, 414 # Summary 415 "net_profit": round(self.net_profit, 2), 416 "net_profit_pct": round(self.net_profit_pct, 2), 417 "final_equity": round(self.final_equity, 2), 418 # Trade Statistics 419 "total_trades": self.total_trades, 420 "winning_trades": self.winning_trades, 421 "losing_trades": self.losing_trades, 422 "win_rate": round(self.win_rate, 2), 423 "profit_factor": round(self.profit_factor, 2) if self.profit_factor != float("inf") else "inf", 424 "avg_trade": round(self.avg_trade, 2), 425 "avg_winning_trade": round(self.avg_winning_trade, 2), 426 "avg_losing_trade": round(self.avg_losing_trade, 2), 427 "max_consecutive_wins": self.max_consecutive_wins, 428 "max_consecutive_losses": self.max_consecutive_losses, 429 # Risk Metrics 430 "sharpe_ratio": round(self.sharpe_ratio, 2) if not np.isnan(self.sharpe_ratio) else None, 431 "sortino_ratio": round(self.sortino_ratio, 2) if not np.isnan(self.sortino_ratio) and self.sortino_ratio != float("inf") else None, 432 "calmar_ratio": round(self.calmar_ratio, 2) if self.calmar_ratio != float("inf") else None, 433 "max_drawdown": round(self.max_drawdown, 2), 434 "max_drawdown_duration": self.max_drawdown_duration, 435 # Comparison 436 "buy_hold_return": round(self.buy_hold_return, 2), 437 "vs_buy_hold": round(self.vs_buy_hold, 2), 438 }
Export results to dictionary.
Compatible with TradingView/Mathieu2301 format.
440 def summary(self) -> str: 441 """ 442 Generate human-readable performance summary. 443 444 Returns: 445 Formatted summary string. 446 """ 447 d = self.to_dict() 448 449 lines = [ 450 "=" * 60, 451 f"BACKTEST RESULTS: {d['symbol']} ({d['strategy_name']})", 452 "=" * 60, 453 f"Period: {d['period']} | Interval: {d['interval']}", 454 f"Initial Capital: {d['initial_capital']:,.2f} TL", 455 f"Commission: {d['commission']*100:.2f}%", 456 "", 457 "--- PERFORMANCE ---", 458 f"Net Profit: {d['net_profit']:,.2f} TL ({d['net_profit_pct']:+.2f}%)", 459 f"Final Equity: {d['final_equity']:,.2f} TL", 460 f"Buy & Hold: {d['buy_hold_return']:+.2f}%", 461 f"vs B&H: {d['vs_buy_hold']:+.2f}%", 462 "", 463 "--- TRADE STATISTICS ---", 464 f"Total Trades: {d['total_trades']}", 465 f"Winning: {d['winning_trades']} | Losing: {d['losing_trades']}", 466 f"Win Rate: {d['win_rate']:.1f}%", 467 f"Profit Factor: {d['profit_factor']}", 468 f"Avg Trade: {d['avg_trade']:,.2f} TL", 469 f"Avg Winner: {d['avg_winning_trade']:,.2f} TL | Avg Loser: {d['avg_losing_trade']:,.2f} TL", 470 f"Max Consecutive Wins: {d['max_consecutive_wins']} | Losses: {d['max_consecutive_losses']}", 471 "", 472 "--- RISK METRICS ---", 473 f"Sharpe Ratio: {d['sharpe_ratio'] if d['sharpe_ratio'] else 'N/A'}", 474 f"Sortino Ratio: {d['sortino_ratio'] if d['sortino_ratio'] else 'N/A'}", 475 f"Calmar Ratio: {d['calmar_ratio'] if d['calmar_ratio'] else 'N/A'}", 476 f"Max Drawdown: {d['max_drawdown']:.2f}%", 477 f"Max DD Duration: {d['max_drawdown_duration']} days", 478 "=" * 60, 479 ] 480 481 return "\n".join(lines)
Generate human-readable performance summary.
Returns: Formatted summary string.
51@dataclass 52class Trade: 53 """ 54 Represents a single trade in a backtest. 55 56 Attributes: 57 entry_time: When the trade was opened. 58 entry_price: Price at entry. 59 exit_time: When the trade was closed (None if open). 60 exit_price: Price at exit (None if open). 61 side: Trade direction ('long' or 'short'). 62 shares: Number of shares traded. 63 commission: Total commission paid (entry + exit). 64 """ 65 66 entry_time: datetime 67 entry_price: float 68 exit_time: datetime | None = None 69 exit_price: float | None = None 70 side: Literal["long", "short"] = "long" 71 shares: float = 0.0 72 commission: float = 0.0 73 74 @property 75 def is_closed(self) -> bool: 76 """Check if trade is closed.""" 77 return self.exit_time is not None and self.exit_price is not None 78 79 @property 80 def profit(self) -> float | None: 81 """Calculate profit in currency units (None if open).""" 82 if not self.is_closed: 83 return None 84 assert self.exit_price is not None 85 if self.side == "long": 86 gross = (self.exit_price - self.entry_price) * self.shares 87 else: 88 gross = (self.entry_price - self.exit_price) * self.shares 89 return gross - self.commission 90 91 @property 92 def profit_pct(self) -> float | None: 93 """Calculate profit as percentage (None if open).""" 94 if not self.is_closed or self.entry_price == 0: 95 return None 96 profit = self.profit 97 if profit is None: 98 return None 99 entry_value = self.entry_price * self.shares 100 return (profit / entry_value) * 100 101 102 @property 103 def duration(self) -> float | None: 104 """Trade duration in days (None if open).""" 105 if not self.is_closed: 106 return None 107 assert self.exit_time is not None 108 delta = self.exit_time - self.entry_time 109 return delta.total_seconds() / 86400 # Convert to days 110 111 def to_dict(self) -> dict[str, Any]: 112 """Convert trade to dictionary.""" 113 return { 114 "entry_time": self.entry_time, 115 "entry_price": self.entry_price, 116 "exit_time": self.exit_time, 117 "exit_price": self.exit_price, 118 "side": self.side, 119 "shares": self.shares, 120 "commission": self.commission, 121 "profit": self.profit, 122 "profit_pct": self.profit_pct, 123 "duration": self.duration, 124 }
Represents a single trade in a backtest.
Attributes: entry_time: When the trade was opened. entry_price: Price at entry. exit_time: When the trade was closed (None if open). exit_price: Price at exit (None if open). side: Trade direction ('long' or 'short'). shares: Number of shares traded. commission: Total commission paid (entry + exit).
74 @property 75 def is_closed(self) -> bool: 76 """Check if trade is closed.""" 77 return self.exit_time is not None and self.exit_price is not None
Check if trade is closed.
79 @property 80 def profit(self) -> float | None: 81 """Calculate profit in currency units (None if open).""" 82 if not self.is_closed: 83 return None 84 assert self.exit_price is not None 85 if self.side == "long": 86 gross = (self.exit_price - self.entry_price) * self.shares 87 else: 88 gross = (self.entry_price - self.exit_price) * self.shares 89 return gross - self.commission
Calculate profit in currency units (None if open).
91 @property 92 def profit_pct(self) -> float | None: 93 """Calculate profit as percentage (None if open).""" 94 if not self.is_closed or self.entry_price == 0: 95 return None 96 profit = self.profit 97 if profit is None: 98 return None 99 entry_value = self.entry_price * self.shares 100 return (profit / entry_value) * 100
Calculate profit as percentage (None if open).
102 @property 103 def duration(self) -> float | None: 104 """Trade duration in days (None if open).""" 105 if not self.is_closed: 106 return None 107 assert self.exit_time is not None 108 delta = self.exit_time - self.entry_time 109 return delta.total_seconds() / 86400 # Convert to days
Trade duration in days (None if open).
111 def to_dict(self) -> dict[str, Any]: 112 """Convert trade to dictionary.""" 113 return { 114 "entry_time": self.entry_time, 115 "entry_price": self.entry_price, 116 "exit_time": self.exit_time, 117 "exit_price": self.exit_price, 118 "side": self.side, 119 "shares": self.shares, 120 "commission": self.commission, 121 "profit": self.profit, 122 "profit_pct": self.profit_pct, 123 "duration": self.duration, 124 }
Convert trade to dictionary.
816def backtest( 817 symbol: str, 818 strategy: StrategyFunc, 819 period: str = "1y", 820 interval: str = "1d", 821 capital: float = 100_000.0, 822 commission: float = 0.001, 823 indicators: list[str] | None = None, 824) -> BacktestResult: 825 """ 826 Run a backtest with a single function call. 827 828 Convenience function that creates a Backtest instance and runs it. 829 830 Args: 831 symbol: Stock symbol (e.g., "THYAO"). 832 strategy: Strategy function with signature: 833 strategy(candle, position, indicators) -> 'BUY'|'SELL'|'HOLD'|None 834 period: Historical data period. 835 interval: Data interval. 836 capital: Initial capital. 837 commission: Commission rate. 838 indicators: List of indicators to calculate. 839 840 Returns: 841 BacktestResult with all performance metrics. 842 843 Examples: 844 >>> def rsi_strategy(candle, position, indicators): 845 ... if indicators.get('rsi', 50) < 30 and position is None: 846 ... return 'BUY' 847 ... elif indicators.get('rsi', 50) > 70 and position == 'long': 848 ... return 'SELL' 849 ... return 'HOLD' 850 851 >>> result = bp.backtest("THYAO", rsi_strategy, period="1y") 852 >>> print(f"Net Profit: {result.net_profit_pct:.2f}%") 853 >>> print(f"Sharpe: {result.sharpe_ratio:.2f}") 854 """ 855 bt = Backtest( 856 symbol=symbol, 857 strategy=strategy, 858 period=period, 859 interval=interval, 860 capital=capital, 861 commission=commission, 862 indicators=indicators, 863 ) 864 return bt.run()
Run a backtest with a single function call.
Convenience function that creates a Backtest instance and runs it.
Args: symbol: Stock symbol (e.g., "THYAO"). strategy: Strategy function with signature: strategy(candle, position, indicators) -> 'BUY'|'SELL'|'HOLD'|None period: Historical data period. interval: Data interval. capital: Initial capital. commission: Commission rate. indicators: List of indicators to calculate.
Returns: BacktestResult with all performance metrics.
Examples:
def rsi_strategy(candle, position, indicators): ... if indicators.get('rsi', 50) < 30 and position is None: ... return 'BUY' ... elif indicators.get('rsi', 50) > 70 and position == 'long': ... return 'SELL' ... return 'HOLD'
>>> result = bp.backtest("THYAO", rsi_strategy, period="1y") >>> print(f"Net Profit: {result.net_profit_pct:.2f}%") >>> print(f"Sharpe: {result.sharpe_ratio:.2f}")