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) 126 127# Twitter/X authentication (optional, requires borsapy[twitter]) 128from borsapy._providers.twitter import ( 129 clear_twitter_auth, 130 get_twitter_auth, 131 set_twitter_auth, 132) 133from borsapy.backtest import Backtest, BacktestResult, Trade, backtest 134from borsapy.bond import Bond, bonds, risk_free_rate 135from borsapy.calendar import EconomicCalendar, economic_calendar 136from borsapy.charts import calculate_heikin_ashi 137from borsapy.crypto import Crypto, crypto_pairs 138from borsapy.eurobond import Eurobond, eurobonds 139from borsapy.exceptions import ( 140 APIError, 141 AuthenticationError, 142 BorsapyError, 143 DataNotAvailableError, 144 InvalidIntervalError, 145 InvalidPeriodError, 146 RateLimitError, 147 TickerNotFoundError, 148) 149from borsapy.fund import Fund, compare_funds, management_fees, screen_funds, search_funds 150from borsapy.fx import FX, banks, metal_institutions 151from borsapy.index import Index, all_indices, index, indices 152from borsapy.inflation import Inflation 153from borsapy.market import companies, search_companies 154from borsapy.multi import Tickers, download 155from borsapy.portfolio import Portfolio 156from borsapy.replay import ReplaySession, create_replay 157from borsapy.scanner import ScanResult, TechnicalScanner, scan 158from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices 159from borsapy.search import ( 160 search, 161 search_bist, 162 search_crypto, 163 search_forex, 164 search_index, 165 search_viop, 166 viop_contracts, 167) 168 169# TradingView streaming for real-time updates 170from borsapy.stream import TradingViewStream, create_stream 171from borsapy.tax import withholding_tax_rate, withholding_tax_table 172from borsapy.tcmb import TCMB, policy_rate 173from borsapy.technical import ( 174 TechnicalAnalyzer, 175 add_indicators, 176 calculate_adx, 177 calculate_atr, 178 calculate_bollinger_bands, 179 calculate_dema, 180 calculate_ema, 181 calculate_hhv, 182 calculate_llv, 183 calculate_macd, 184 calculate_mom, 185 calculate_obv, 186 calculate_roc, 187 calculate_rsi, 188 calculate_sma, 189 calculate_stochastic, 190 calculate_supertrend, 191 calculate_tema, 192 calculate_tilson_t3, 193 calculate_vwap, 194 calculate_wma, 195) 196from borsapy.ticker import Ticker 197from borsapy.twitter import search_tweets 198from borsapy.viop import VIOP 199 200__version__ = "0.8.4" 201__author__ = "Said Surucu" 202 203__all__ = [ 204 # Main classes 205 "Ticker", 206 "Tickers", 207 "FX", 208 "Crypto", 209 "Fund", 210 "Portfolio", 211 "Index", 212 "Inflation", 213 "VIOP", 214 "Bond", 215 "Eurobond", 216 "TCMB", 217 "EconomicCalendar", 218 "Screener", 219 "TradingViewStream", 220 "ReplaySession", 221 # Market functions 222 "companies", 223 "search_companies", 224 "search", 225 "search_bist", 226 "search_crypto", 227 "search_forex", 228 "search_index", 229 "search_viop", 230 "viop_contracts", 231 "banks", 232 "metal_institutions", 233 "crypto_pairs", 234 "search_funds", 235 "screen_funds", 236 "compare_funds", 237 "management_fees", 238 "download", 239 "index", 240 "indices", 241 "all_indices", 242 # Bond functions 243 "bonds", 244 "risk_free_rate", 245 # Eurobond functions 246 "eurobonds", 247 # TCMB functions 248 "policy_rate", 249 # Calendar functions 250 "economic_calendar", 251 # Screener functions 252 "screen_stocks", 253 "screener_criteria", 254 "sectors", 255 "stock_indices", 256 # Technical Scanner 257 "TechnicalScanner", 258 "ScanResult", 259 "scan", 260 # Technical analysis 261 "TechnicalAnalyzer", 262 "add_indicators", 263 "calculate_sma", 264 "calculate_ema", 265 "calculate_rsi", 266 "calculate_macd", 267 "calculate_bollinger_bands", 268 "calculate_atr", 269 "calculate_stochastic", 270 "calculate_obv", 271 "calculate_vwap", 272 "calculate_adx", 273 "calculate_supertrend", 274 "calculate_tilson_t3", 275 # MetaStock indicators 276 "calculate_hhv", 277 "calculate_llv", 278 "calculate_mom", 279 "calculate_roc", 280 "calculate_wma", 281 "calculate_dema", 282 "calculate_tema", 283 # Charts 284 "calculate_heikin_ashi", 285 # Replay 286 "ReplaySession", 287 "create_replay", 288 # Exceptions 289 "BorsapyError", 290 "TickerNotFoundError", 291 "DataNotAvailableError", 292 "APIError", 293 "AuthenticationError", 294 "RateLimitError", 295 "InvalidPeriodError", 296 "InvalidIntervalError", 297 # TradingView authentication (premium) 298 "set_tradingview_auth", 299 "get_tradingview_auth", 300 "clear_tradingview_auth", 301 # TradingView streaming (real-time) 302 "TradingViewStream", 303 "create_stream", 304 # Backtest engine 305 "Backtest", 306 "BacktestResult", 307 "Trade", 308 "backtest", 309 # Tax 310 "withholding_tax_rate", 311 "withholding_tax_table", 312 # Twitter/X (optional, requires borsapy[twitter]) 313 "set_twitter_auth", 314 "get_twitter_auth", 315 "clear_twitter_auth", 316 "search_tweets", 317]
469class Ticker(TechnicalMixin, TwitterMixin): 470 """ 471 A yfinance-like interface for Turkish stock data. 472 473 Examples: 474 >>> import borsapy as bp 475 >>> stock = bp.Ticker("THYAO") 476 >>> stock.info 477 {'symbol': 'THYAO', 'last': 268.5, ...} 478 >>> stock.history(period="1mo") 479 Open High Low Close Volume 480 Date 481 2024-12-01 265.00 268.00 264.00 267.50 12345678 482 ... 483 """ 484 485 def __init__(self, symbol: str): 486 """ 487 Initialize a Ticker object. 488 489 Args: 490 symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). 491 The ".IS" or ".E" suffix is optional and will be removed. 492 """ 493 self._symbol = symbol.upper().replace(".IS", "").replace(".E", "") 494 self._tradingview = get_tradingview_provider() 495 self._isyatirim = None # Lazy load for financial statements 496 self._kap = None # Lazy load for KAP disclosures 497 self._isin_provider = None # Lazy load for ISIN lookup 498 self._hedeffiyat = None # Lazy load for analyst price targets 499 self._etf_provider = None # Lazy load for ETF holders 500 501 def _get_tweet_query(self) -> str: 502 return _build_stock_query(self._symbol) 503 504 def _get_isyatirim(self): 505 """Lazy load İş Yatırım provider for financial statements.""" 506 if self._isyatirim is None: 507 from borsapy._providers.isyatirim import get_isyatirim_provider 508 509 self._isyatirim = get_isyatirim_provider() 510 return self._isyatirim 511 512 def _get_kap(self): 513 """Lazy load KAP provider for disclosures and calendar.""" 514 if self._kap is None: 515 from borsapy._providers.kap import get_kap_provider 516 517 self._kap = get_kap_provider() 518 return self._kap 519 520 def _get_isin_provider(self): 521 """Lazy load ISIN provider.""" 522 if self._isin_provider is None: 523 from borsapy._providers.isin import get_isin_provider 524 525 self._isin_provider = get_isin_provider() 526 return self._isin_provider 527 528 def _get_hedeffiyat(self): 529 """Lazy load hedeffiyat.com.tr provider for analyst price targets.""" 530 if self._hedeffiyat is None: 531 from borsapy._providers.hedeffiyat import get_hedeffiyat_provider 532 533 self._hedeffiyat = get_hedeffiyat_provider() 534 return self._hedeffiyat 535 536 def _get_etf_provider(self): 537 """Lazy load TradingView ETF provider for ETF holders.""" 538 if self._etf_provider is None: 539 from borsapy._providers.tradingview_etf import get_tradingview_etf_provider 540 541 self._etf_provider = get_tradingview_etf_provider() 542 return self._etf_provider 543 544 @property 545 def symbol(self) -> str: 546 """Return the ticker symbol.""" 547 return self._symbol 548 549 @property 550 def fast_info(self) -> FastInfo: 551 """ 552 Get fast access to common ticker information. 553 554 Returns a FastInfo object with quick access to frequently used data: 555 - currency, exchange, timezone 556 - last_price, open, day_high, day_low, previous_close, volume 557 - market_cap, shares, pe_ratio, pb_ratio 558 - year_high, year_low (52-week) 559 - fifty_day_average, two_hundred_day_average 560 - free_float, foreign_ratio 561 562 Examples: 563 >>> stock = Ticker("THYAO") 564 >>> stock.fast_info.market_cap 565 370530000000 566 >>> stock.fast_info['pe_ratio'] 567 2.8 568 >>> stock.fast_info.keys() 569 ['currency', 'exchange', 'timezone', ...] 570 """ 571 if not hasattr(self, "_fast_info"): 572 self._fast_info = FastInfo(self) 573 return self._fast_info 574 575 @property 576 def info(self) -> EnrichedInfo: 577 """ 578 Get comprehensive ticker information with yfinance-compatible fields. 579 580 Returns: 581 EnrichedInfo object providing dict-like access to: 582 583 Basic fields (always loaded, fast): 584 - symbol, last, open, high, low, close, volume 585 - change, change_percent, update_time 586 587 yfinance aliases (map to basic fields): 588 - regularMarketPrice, currentPrice -> last 589 - regularMarketOpen -> open 590 - regularMarketDayHigh -> high 591 - regularMarketDayLow -> low 592 - regularMarketPreviousClose -> close 593 - regularMarketVolume -> volume 594 595 Extended fields (lazy-loaded on access): 596 - marketCap, trailingPE, priceToBook, enterpriseToEbitda 597 - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow 598 - fiftyDayAverage, twoHundredDayAverage 599 - floatShares, foreignRatio, netDebt 600 - currency, exchange, timezone 601 602 Dividend fields (lazy-loaded on access): 603 - dividendYield, exDividendDate 604 - trailingAnnualDividendRate, trailingAnnualDividendYield 605 606 Examples: 607 >>> stock = Ticker("THYAO") 608 >>> stock.info['last'] # Basic field - fast 609 268.5 610 >>> stock.info['marketCap'] # Extended field - fetches İş Yatırım 611 370530000000 612 >>> stock.info['trailingPE'] # yfinance compatible name 613 2.8 614 >>> stock.info.get('dividendYield') # Safe access 615 1.28 616 >>> stock.info.todict() # Get all as regular dict 617 {...} 618 """ 619 if not hasattr(self, "_enriched_info"): 620 self._enriched_info = EnrichedInfo(self) 621 return self._enriched_info 622 623 def history( 624 self, 625 period: str = "1mo", 626 interval: str = "1d", 627 start: datetime | str | None = None, 628 end: datetime | str | None = None, 629 actions: bool = False, 630 adjust: bool = True, 631 ) -> pd.DataFrame: 632 """ 633 Get historical OHLCV data. 634 635 Args: 636 period: How much data to fetch. Valid periods: 637 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. 638 Ignored if start is provided. 639 interval: Data granularity. Valid intervals: 640 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 641 start: Start date (string or datetime). 642 end: End date (string or datetime). Defaults to today. 643 actions: If True, include Dividends and Stock Splits columns. 644 Defaults to False. 645 adjust: If True (default), return split-adjusted prices. 646 If False, return unadjusted (raw) prices. 647 648 Returns: 649 DataFrame with columns: Open, High, Low, Close, Volume. 650 If actions=True, also includes Dividends and Stock Splits columns. 651 Index is the Date. 652 653 Examples: 654 >>> stock = Ticker("THYAO") 655 >>> stock.history(period="1mo") # Last month 656 >>> stock.history(period="1y", interval="1wk") # Weekly for 1 year 657 >>> stock.history(start="2024-01-01", end="2024-06-30") # Date range 658 >>> stock.history(period="1y", actions=True) # With dividends/splits 659 >>> stock.history(period="max", adjust=False) # Raw unadjusted prices 660 """ 661 # Parse dates if strings 662 start_dt = self._parse_date(start) if start else None 663 end_dt = self._parse_date(end) if end else None 664 665 df = self._tradingview.get_history( 666 symbol=self._symbol, 667 period=period, 668 interval=interval, 669 start=start_dt, 670 end=end_dt, 671 ) 672 673 if not adjust and not df.empty: 674 df = self._unadjust_prices(df) 675 676 if actions and not df.empty: 677 df = self._add_actions_to_history(df) 678 679 return df 680 681 def _add_actions_to_history(self, df: pd.DataFrame) -> pd.DataFrame: 682 """ 683 Add Dividends and Stock Splits columns to historical data. 684 685 Args: 686 df: Historical OHLCV DataFrame. 687 688 Returns: 689 DataFrame with added Dividends and Stock Splits columns. 690 """ 691 # Initialize columns with zeros 692 df = df.copy() 693 df["Dividends"] = 0.0 694 df["Stock Splits"] = 0.0 695 696 # Get dividends 697 try: 698 divs = self.dividends 699 if not divs.empty: 700 for div_date, row in divs.iterrows(): 701 # Use date() for timezone-agnostic comparison 702 div_date_only = pd.Timestamp(div_date).date() 703 for idx in df.index: 704 idx_date_only = pd.Timestamp(idx).date() 705 if div_date_only == idx_date_only: 706 df.loc[idx, "Dividends"] = row.get("Amount", 0) 707 break 708 except Exception: 709 pass 710 711 # Get stock splits (capital increases) 712 try: 713 splits = self.splits 714 if not splits.empty: 715 for split_date, row in splits.iterrows(): 716 # Use date() for timezone-agnostic comparison 717 split_date_only = pd.Timestamp(split_date).date() 718 # Calculate split ratio 719 # BonusFromCapital + BonusFromDividend = total bonus percentage 720 bonus_pct = row.get("BonusFromCapital", 0) + row.get( 721 "BonusFromDividend", 0 722 ) 723 if bonus_pct > 0: 724 # Convert percentage to split ratio (e.g., 20% bonus = 1.2 split) 725 split_ratio = 1 + (bonus_pct / 100) 726 for idx in df.index: 727 idx_date_only = pd.Timestamp(idx).date() 728 if split_date_only == idx_date_only: 729 df.loc[idx, "Stock Splits"] = split_ratio 730 break 731 except Exception: 732 pass 733 734 return df 735 736 def _unadjust_prices(self, df: pd.DataFrame) -> pd.DataFrame: 737 """ 738 Reverse split adjustments to return raw unadjusted prices. 739 740 TradingView returns split-adjusted prices by default. This method 741 uses the splits data from İş Yatırım to reverse the adjustments. 742 """ 743 try: 744 splits = self.splits 745 except Exception: 746 return df 747 748 if splits.empty: 749 return df 750 751 # Collect split events with bonus ratios 752 split_events = [] 753 for split_date, row in splits.iterrows(): 754 bonus_pct = row.get("BonusFromCapital", 0) + row.get( 755 "BonusFromDividend", 0 756 ) 757 if bonus_pct > 0: 758 ratio = 1 + (bonus_pct / 100) 759 split_events.append( 760 (pd.Timestamp(split_date, tz="Europe/Istanbul"), ratio) 761 ) 762 763 if not split_events: 764 return df 765 766 # Sort by date ascending 767 split_events.sort(key=lambda x: x[0]) 768 769 df = df.copy() 770 771 # Build a vectorized adjustment factor series. 772 # For each split, all rows BEFORE the split date get multiplied by the ratio. 773 factor = pd.Series(1.0, index=df.index) 774 for split_date, ratio in split_events: 775 factor = factor * pd.Series( 776 [ratio if idx < split_date else 1.0 for idx in df.index], 777 index=df.index, 778 ) 779 780 price_cols = [c for c in ["Open", "High", "Low", "Close"] if c in df.columns] 781 for col in price_cols: 782 df[col] = df[col] * factor 783 if "Volume" in df.columns: 784 df["Volume"] = df["Volume"] / factor 785 786 return df 787 788 @cached_property 789 def dividends(self) -> pd.DataFrame: 790 """ 791 Get dividend history. 792 793 Returns: 794 DataFrame with dividend history: 795 - Amount: Dividend per share (TL) 796 - GrossRate: Gross dividend rate (%) 797 - NetRate: Net dividend rate (%) 798 - TotalDividend: Total dividend distributed (TL) 799 800 Examples: 801 >>> stock = Ticker("THYAO") 802 >>> stock.dividends 803 Amount GrossRate NetRate TotalDividend 804 Date 805 2025-09-02 3.442 344.20 292.57 4750000000.0 806 2025-06-16 3.442 344.20 292.57 4750000000.0 807 """ 808 return self._get_isyatirim().get_dividends(self._symbol) 809 810 @cached_property 811 def splits(self) -> pd.DataFrame: 812 """ 813 Get capital increase (split) history. 814 815 Note: Turkish market uses capital increases instead of traditional splits. 816 - RightsIssue: Paid capital increase (bedelli) 817 - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak) 818 - BonusFromDividend: Free shares from dividend (bedelsiz temettüden) 819 820 Returns: 821 DataFrame with capital increase history: 822 - Capital: New capital after increase (TL) 823 - RightsIssue: Rights issue rate (%) 824 - BonusFromCapital: Bonus from capital (%) 825 - BonusFromDividend: Bonus from dividend (%) 826 827 Examples: 828 >>> stock = Ticker("THYAO") 829 >>> stock.splits 830 Capital RightsIssue BonusFromCapital BonusFromDividend 831 Date 832 2013-06-26 1380000000.0 0.0 15.00 0.0 833 2011-07-11 1200000000.0 0.0 0.00 20.0 834 """ 835 return self._get_isyatirim().get_capital_increases(self._symbol) 836 837 @cached_property 838 def actions(self) -> pd.DataFrame: 839 """ 840 Get combined dividends and splits history. 841 842 Returns: 843 DataFrame with combined dividend and split actions: 844 - Dividends: Dividend per share (TL) or 0 845 - Splits: Combined split ratio (0 if no split) 846 847 Examples: 848 >>> stock = Ticker("THYAO") 849 >>> stock.actions 850 Dividends Splits 851 Date 852 2025-09-02 3.442 0.0 853 2013-06-26 0.000 15.0 854 """ 855 dividends = self.dividends 856 splits = self.splits 857 858 # Merge on index (Date) 859 if dividends.empty and splits.empty: 860 return pd.DataFrame(columns=["Dividends", "Splits"]) 861 862 # Extract relevant columns 863 div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float) 864 split_series = ( 865 splits["BonusFromCapital"] + splits["BonusFromDividend"] 866 if not splits.empty 867 else pd.Series(dtype=float) 868 ) 869 870 # Combine into single DataFrame 871 result = pd.DataFrame({"Dividends": div_series, "Splits": split_series}) 872 result = result.fillna(0) 873 result = result.sort_index(ascending=False) 874 875 return result 876 877 def get_balance_sheet( 878 self, 879 quarterly: bool = False, 880 financial_group: str | None = None, 881 last_n: int | str | None = None, 882 ) -> pd.DataFrame: 883 """ 884 Get balance sheet data. 885 886 Args: 887 quarterly: If True, return quarterly data. If False, return annual. 888 financial_group: Financial group code. Use "UFRS" for banks, 889 "XI_29" for industrial companies. If None, defaults to XI_29. 890 last_n: Number of periods to fetch. None for default (5), int for exact 891 count (e.g. 10 = 10 annual periods), "all" for maximum available. 892 893 Returns: 894 DataFrame with balance sheet items as rows and periods as columns. 895 896 Examples: 897 >>> stock = bp.Ticker("THYAO") 898 >>> stock.get_balance_sheet() # Annual, industrial (5 periods) 899 >>> stock.get_balance_sheet(quarterly=True, last_n=20) # 20 quarters 900 901 >>> bank = bp.Ticker("AKBNK") 902 >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all") 903 """ 904 return self._get_isyatirim().get_financial_statements( 905 symbol=self._symbol, 906 statement_type="balance_sheet", 907 quarterly=quarterly, 908 financial_group=financial_group, 909 last_n=last_n, 910 ) 911 912 def get_income_stmt( 913 self, 914 quarterly: bool = False, 915 financial_group: str | None = None, 916 last_n: int | str | None = None, 917 ) -> pd.DataFrame: 918 """ 919 Get income statement data. 920 921 Args: 922 quarterly: If True, return quarterly data. If False, return annual. 923 financial_group: Financial group code. Use "UFRS" for banks, 924 "XI_29" for industrial companies. If None, defaults to XI_29. 925 last_n: Number of periods to fetch. None for default (5), int for exact 926 count (e.g. 10 = 10 annual periods), "all" for maximum available. 927 928 Returns: 929 DataFrame with income statement items as rows and periods as columns. 930 931 Examples: 932 >>> stock = bp.Ticker("THYAO") 933 >>> stock.get_income_stmt() # Annual (5 periods) 934 >>> stock.get_income_stmt(quarterly=True, last_n=20) # 20 quarters 935 936 >>> bank = bp.Ticker("AKBNK") 937 >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS") 938 """ 939 return self._get_isyatirim().get_financial_statements( 940 symbol=self._symbol, 941 statement_type="income_stmt", 942 quarterly=quarterly, 943 financial_group=financial_group, 944 last_n=last_n, 945 ) 946 947 def get_cashflow( 948 self, 949 quarterly: bool = False, 950 financial_group: str | None = None, 951 last_n: int | str | None = None, 952 ) -> pd.DataFrame: 953 """ 954 Get cash flow statement data. 955 956 Args: 957 quarterly: If True, return quarterly data. If False, return annual. 958 financial_group: Financial group code. Use "UFRS" for banks, 959 "XI_29" for industrial companies. If None, defaults to XI_29. 960 last_n: Number of periods to fetch. None for default (5), int for exact 961 count (e.g. 10 = 10 annual periods), "all" for maximum available. 962 963 Returns: 964 DataFrame with cash flow items as rows and periods as columns. 965 966 Examples: 967 >>> stock = bp.Ticker("THYAO") 968 >>> stock.get_cashflow() # Annual (5 periods) 969 >>> stock.get_cashflow(quarterly=True, last_n=20) # 20 quarters 970 971 >>> bank = bp.Ticker("AKBNK") 972 >>> bank.get_cashflow(financial_group="UFRS", last_n="all") 973 """ 974 return self._get_isyatirim().get_financial_statements( 975 symbol=self._symbol, 976 statement_type="cashflow", 977 quarterly=quarterly, 978 financial_group=financial_group, 979 last_n=last_n, 980 ) 981 982 # Legacy property aliases for backward compatibility 983 @cached_property 984 def balance_sheet(self) -> pd.DataFrame: 985 """Annual balance sheet (use get_balance_sheet() for more options).""" 986 return self.get_balance_sheet(quarterly=False) 987 988 @cached_property 989 def quarterly_balance_sheet(self) -> pd.DataFrame: 990 """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).""" 991 return self.get_balance_sheet(quarterly=True) 992 993 @cached_property 994 def income_stmt(self) -> pd.DataFrame: 995 """Annual income statement (use get_income_stmt() for more options).""" 996 return self.get_income_stmt(quarterly=False) 997 998 @cached_property 999 def quarterly_income_stmt(self) -> pd.DataFrame: 1000 """Quarterly income statement (use get_income_stmt(quarterly=True) for more options).""" 1001 return self.get_income_stmt(quarterly=True) 1002 1003 @cached_property 1004 def cashflow(self) -> pd.DataFrame: 1005 """Annual cash flow (use get_cashflow() for more options).""" 1006 return self.get_cashflow(quarterly=False) 1007 1008 @cached_property 1009 def quarterly_cashflow(self) -> pd.DataFrame: 1010 """Quarterly cash flow (use get_cashflow(quarterly=True) for more options).""" 1011 return self.get_cashflow(quarterly=True) 1012 1013 def _calculate_ttm(self, quarterly_df: pd.DataFrame) -> pd.DataFrame: 1014 """ 1015 Calculate trailing twelve months (TTM) by summing last 4 quarters. 1016 1017 Args: 1018 quarterly_df: DataFrame with quarterly data (columns in YYYYQN format). 1019 1020 Returns: 1021 DataFrame with single TTM column containing summed values. 1022 """ 1023 if quarterly_df.empty or len(quarterly_df.columns) < 4: 1024 return pd.DataFrame(columns=["TTM"]) 1025 1026 # First 4 columns = last 4 quarters (most recent first) 1027 last_4_quarters = quarterly_df.iloc[:, :4] 1028 1029 # Convert to numeric, coercing errors to NaN 1030 numeric_df = last_4_quarters.apply(pd.to_numeric, errors="coerce") 1031 1032 return numeric_df.sum(axis=1).to_frame(name="TTM") 1033 1034 def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame: 1035 """ 1036 Get trailing twelve months (TTM) income statement. 1037 1038 Calculates TTM by summing the last 4 quarters of income statement data. 1039 1040 Args: 1041 financial_group: Financial group code. Use "UFRS" for banks, 1042 "XI_29" for industrial companies. If None, defaults to XI_29. 1043 1044 Returns: 1045 DataFrame with TTM column containing summed values for each line item. 1046 1047 Examples: 1048 >>> stock = bp.Ticker("THYAO") 1049 >>> stock.get_ttm_income_stmt() 1050 1051 >>> bank = bp.Ticker("AKBNK") 1052 >>> bank.get_ttm_income_stmt(financial_group="UFRS") 1053 """ 1054 quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group) 1055 return self._calculate_ttm(quarterly) 1056 1057 def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame: 1058 """ 1059 Get trailing twelve months (TTM) cash flow statement. 1060 1061 Calculates TTM by summing the last 4 quarters of cash flow data. 1062 1063 Args: 1064 financial_group: Financial group code. Use "UFRS" for banks, 1065 "XI_29" for industrial companies. If None, defaults to XI_29. 1066 1067 Returns: 1068 DataFrame with TTM column containing summed values for each line item. 1069 1070 Examples: 1071 >>> stock = bp.Ticker("THYAO") 1072 >>> stock.get_ttm_cashflow() 1073 1074 >>> bank = bp.Ticker("AKBNK") 1075 >>> bank.get_ttm_cashflow(financial_group="UFRS") 1076 """ 1077 quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group) 1078 return self._calculate_ttm(quarterly) 1079 1080 # Legacy property aliases 1081 @cached_property 1082 def ttm_income_stmt(self) -> pd.DataFrame: 1083 """TTM income statement (use get_ttm_income_stmt() for banks).""" 1084 return self.get_ttm_income_stmt() 1085 1086 @cached_property 1087 def ttm_cashflow(self) -> pd.DataFrame: 1088 """TTM cash flow (use get_ttm_cashflow() for banks).""" 1089 return self.get_ttm_cashflow() 1090 1091 @cached_property 1092 def major_holders(self) -> pd.DataFrame: 1093 """ 1094 Get major shareholders (ortaklık yapısı). 1095 1096 Returns: 1097 DataFrame with shareholder names and percentages: 1098 - Index: Holder name 1099 - Percentage: Ownership percentage (%) 1100 1101 Examples: 1102 >>> stock = Ticker("THYAO") 1103 >>> stock.major_holders 1104 Percentage 1105 Holder 1106 Diğer 50.88 1107 Türkiye Varlık Fonu 49.12 1108 """ 1109 return self._get_isyatirim().get_major_holders(self._symbol) 1110 1111 @cached_property 1112 def recommendations(self) -> dict: 1113 """ 1114 Get analyst recommendations and target price. 1115 1116 Returns: 1117 Dictionary with: 1118 - recommendation: Buy/Hold/Sell (AL/TUT/SAT) 1119 - target_price: Analyst target price (TL) 1120 - upside_potential: Expected upside (%) 1121 1122 Examples: 1123 >>> stock = Ticker("THYAO") 1124 >>> stock.recommendations 1125 {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01} 1126 """ 1127 return self._get_isyatirim().get_recommendations(self._symbol) 1128 1129 @cached_property 1130 def recommendations_summary(self) -> dict[str, int]: 1131 """ 1132 Get analyst recommendation summary with buy/hold/sell counts. 1133 1134 Aggregates individual analyst recommendations from hedeffiyat.com.tr 1135 into yfinance-compatible categories. 1136 1137 Returns: 1138 Dictionary with counts: 1139 - strongBuy: Strong buy recommendations 1140 - buy: Buy recommendations (includes "Endeks Üstü Getiri") 1141 - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") 1142 - sell: Sell recommendations (includes "Endeks Altı Getiri") 1143 - strongSell: Strong sell recommendations 1144 1145 Examples: 1146 >>> stock = Ticker("THYAO") 1147 >>> stock.recommendations_summary 1148 {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0} 1149 """ 1150 return self._get_hedeffiyat().get_recommendations_summary(self._symbol) 1151 1152 @cached_property 1153 def news(self) -> pd.DataFrame: 1154 """ 1155 Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock. 1156 1157 Fetches directly from KAP - the official disclosure platform for 1158 publicly traded companies in Turkey. 1159 1160 Returns: 1161 DataFrame with columns: 1162 - Date: Disclosure date and time 1163 - Title: Disclosure headline 1164 - URL: Link to full disclosure on KAP 1165 1166 Examples: 1167 >>> stock = Ticker("THYAO") 1168 >>> stock.news 1169 Date Title URL 1170 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1171 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656 1172 """ 1173 return self._get_kap().get_disclosures(self._symbol) 1174 1175 def get_news_content(self, disclosure_id: int | str) -> str | None: 1176 """ 1177 Get full HTML content of a KAP disclosure by ID. 1178 1179 Args: 1180 disclosure_id: KAP disclosure ID from news DataFrame URL. 1181 1182 Returns: 1183 Raw HTML content or None if failed. 1184 1185 Examples: 1186 >>> stock = Ticker("THYAO") 1187 >>> html = stock.get_news_content(1530826) 1188 """ 1189 return self._get_kap().get_disclosure_content(disclosure_id) 1190 1191 @cached_property 1192 def calendar(self) -> pd.DataFrame: 1193 """ 1194 Get expected disclosure calendar for the stock from KAP. 1195 1196 Returns upcoming expected disclosures like financial reports, 1197 annual reports, sustainability reports, and corporate governance reports. 1198 1199 Returns: 1200 DataFrame with columns: 1201 - StartDate: Expected disclosure window start 1202 - EndDate: Expected disclosure window end 1203 - Subject: Type of disclosure (e.g., "Finansal Rapor") 1204 - Period: Report period (e.g., "Yıllık", "3 Aylık") 1205 - Year: Fiscal year 1206 1207 Examples: 1208 >>> stock = Ticker("THYAO") 1209 >>> stock.calendar 1210 StartDate EndDate Subject Period Year 1211 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1212 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 1213 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026 1214 """ 1215 return self._get_kap().get_calendar(self._symbol) 1216 1217 @cached_property 1218 def isin(self) -> str | None: 1219 """ 1220 Get ISIN (International Securities Identification Number) code. 1221 1222 ISIN is a 12-character alphanumeric code that uniquely identifies 1223 a security, standardized by ISO 6166. 1224 1225 Returns: 1226 ISIN code string (e.g., "TRATHYAO91M5") or None if not found. 1227 1228 Examples: 1229 >>> stock = Ticker("THYAO") 1230 >>> stock.isin 1231 'TRATHYAO91M5' 1232 """ 1233 return self._get_isin_provider().get_isin(self._symbol) 1234 1235 @cached_property 1236 def analyst_price_targets(self) -> dict[str, float | int | None]: 1237 """ 1238 Get analyst price target data from hedeffiyat.com.tr. 1239 1240 Returns aggregated price target information from multiple analysts. 1241 1242 Returns: 1243 Dictionary with: 1244 - current: Current stock price 1245 - low: Lowest analyst target price 1246 - high: Highest analyst target price 1247 - mean: Average target price 1248 - median: Median target price 1249 - numberOfAnalysts: Number of analysts covering the stock 1250 1251 Examples: 1252 >>> stock = Ticker("THYAO") 1253 >>> stock.analyst_price_targets 1254 {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 1255 'median': 465.0, 'numberOfAnalysts': 19} 1256 """ 1257 return self._get_hedeffiyat().get_price_targets(self._symbol) 1258 1259 @property 1260 def etf_holders(self) -> pd.DataFrame: 1261 """ 1262 Get international ETFs that hold this stock. 1263 1264 Returns data from TradingView showing which ETFs hold this stock, 1265 including position value, weight, and ETF characteristics. 1266 1267 Returns: 1268 DataFrame with ETF holder information: 1269 - symbol: ETF ticker symbol 1270 - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) 1271 - name: ETF full name 1272 - market_cap_usd: Position value in USD 1273 - holding_weight_pct: Weight percentage (0.09 = 0.09%) 1274 - issuer: ETF issuer (BlackRock, Vanguard, etc.) 1275 - management: Management style (Passive/Active) 1276 - focus: Investment focus (Total Market, Emerging Markets, etc.) 1277 - expense_ratio: Expense ratio (0.09 = 0.09%) 1278 - aum_usd: Total assets under management (USD) 1279 - price: Current ETF price 1280 - change_pct: Change percentage 1281 1282 Examples: 1283 >>> stock = Ticker("ASELS") 1284 >>> holders = stock.etf_holders 1285 >>> holders[['symbol', 'name', 'holding_weight_pct']].head() 1286 symbol name holding_weight_pct 1287 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1288 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000 1289 1290 >>> print(f"Total ETFs: {len(holders)}") 1291 Total ETFs: 118 1292 """ 1293 return self._get_etf_provider().get_etf_holders(self._symbol) 1294 1295 @cached_property 1296 def earnings_dates(self) -> pd.DataFrame: 1297 """ 1298 Get upcoming earnings announcement dates. 1299 1300 Derived from KAP calendar, showing expected financial report dates. 1301 Compatible with yfinance earnings_dates format. 1302 1303 Returns: 1304 DataFrame with index as Earnings Date and columns: 1305 - EPS Estimate: Always None (not available for BIST) 1306 - Reported EPS: Always None (not available for BIST) 1307 - Surprise (%): Always None (not available for BIST) 1308 1309 Examples: 1310 >>> stock = Ticker("THYAO") 1311 >>> stock.earnings_dates 1312 EPS Estimate Reported EPS Surprise(%) 1313 Earnings Date 1314 2026-03-11 None None None 1315 2026-05-11 None None None 1316 """ 1317 cal = self.calendar 1318 if cal.empty: 1319 return pd.DataFrame( 1320 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1321 ) 1322 1323 # Filter for financial reports only 1324 financial_reports = cal[ 1325 cal["Subject"].str.contains("Finansal Rapor", case=False, na=False) 1326 ] 1327 1328 if financial_reports.empty: 1329 return pd.DataFrame( 1330 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1331 ) 1332 1333 # Use EndDate as the earnings date (latest expected date) 1334 earnings_dates = [] 1335 for _, row in financial_reports.iterrows(): 1336 end_date = row.get("EndDate", "") 1337 if end_date: 1338 try: 1339 # Parse Turkish date format (DD.MM.YYYY) 1340 parsed = datetime.strptime(end_date, "%d.%m.%Y") 1341 earnings_dates.append(parsed) 1342 except ValueError: 1343 continue 1344 1345 if not earnings_dates: 1346 return pd.DataFrame( 1347 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1348 ) 1349 1350 result = pd.DataFrame( 1351 { 1352 "EPS Estimate": [None] * len(earnings_dates), 1353 "Reported EPS": [None] * len(earnings_dates), 1354 "Surprise(%)": [None] * len(earnings_dates), 1355 }, 1356 index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"), 1357 ) 1358 result = result.sort_index() 1359 return result 1360 1361 def _parse_date(self, date: str | datetime) -> datetime: 1362 """Parse a date string to datetime.""" 1363 if isinstance(date, datetime): 1364 return date 1365 # Try common formats 1366 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 1367 try: 1368 return datetime.strptime(date, fmt) 1369 except ValueError: 1370 continue 1371 raise ValueError(f"Could not parse date: {date}") 1372 1373 def _get_ta_symbol_info(self) -> tuple[str, str]: 1374 """Get TradingView symbol and screener for TA signals. 1375 1376 Returns: 1377 Tuple of (tv_symbol, screener) for TradingView Scanner API. 1378 """ 1379 return (f"BIST:{self._symbol}", "turkey") 1380 1381 def __repr__(self) -> str: 1382 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 ...
485 def __init__(self, symbol: str): 486 """ 487 Initialize a Ticker object. 488 489 Args: 490 symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). 491 The ".IS" or ".E" suffix is optional and will be removed. 492 """ 493 self._symbol = symbol.upper().replace(".IS", "").replace(".E", "") 494 self._tradingview = get_tradingview_provider() 495 self._isyatirim = None # Lazy load for financial statements 496 self._kap = None # Lazy load for KAP disclosures 497 self._isin_provider = None # Lazy load for ISIN lookup 498 self._hedeffiyat = None # Lazy load for analyst price targets 499 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.
544 @property 545 def symbol(self) -> str: 546 """Return the ticker symbol.""" 547 return self._symbol
Return the ticker symbol.
549 @property 550 def fast_info(self) -> FastInfo: 551 """ 552 Get fast access to common ticker information. 553 554 Returns a FastInfo object with quick access to frequently used data: 555 - currency, exchange, timezone 556 - last_price, open, day_high, day_low, previous_close, volume 557 - market_cap, shares, pe_ratio, pb_ratio 558 - year_high, year_low (52-week) 559 - fifty_day_average, two_hundred_day_average 560 - free_float, foreign_ratio 561 562 Examples: 563 >>> stock = Ticker("THYAO") 564 >>> stock.fast_info.market_cap 565 370530000000 566 >>> stock.fast_info['pe_ratio'] 567 2.8 568 >>> stock.fast_info.keys() 569 ['currency', 'exchange', 'timezone', ...] 570 """ 571 if not hasattr(self, "_fast_info"): 572 self._fast_info = FastInfo(self) 573 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', ...]
575 @property 576 def info(self) -> EnrichedInfo: 577 """ 578 Get comprehensive ticker information with yfinance-compatible fields. 579 580 Returns: 581 EnrichedInfo object providing dict-like access to: 582 583 Basic fields (always loaded, fast): 584 - symbol, last, open, high, low, close, volume 585 - change, change_percent, update_time 586 587 yfinance aliases (map to basic fields): 588 - regularMarketPrice, currentPrice -> last 589 - regularMarketOpen -> open 590 - regularMarketDayHigh -> high 591 - regularMarketDayLow -> low 592 - regularMarketPreviousClose -> close 593 - regularMarketVolume -> volume 594 595 Extended fields (lazy-loaded on access): 596 - marketCap, trailingPE, priceToBook, enterpriseToEbitda 597 - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow 598 - fiftyDayAverage, twoHundredDayAverage 599 - floatShares, foreignRatio, netDebt 600 - currency, exchange, timezone 601 602 Dividend fields (lazy-loaded on access): 603 - dividendYield, exDividendDate 604 - trailingAnnualDividendRate, trailingAnnualDividendYield 605 606 Examples: 607 >>> stock = Ticker("THYAO") 608 >>> stock.info['last'] # Basic field - fast 609 268.5 610 >>> stock.info['marketCap'] # Extended field - fetches İş Yatırım 611 370530000000 612 >>> stock.info['trailingPE'] # yfinance compatible name 613 2.8 614 >>> stock.info.get('dividendYield') # Safe access 615 1.28 616 >>> stock.info.todict() # Get all as regular dict 617 {...} 618 """ 619 if not hasattr(self, "_enriched_info"): 620 self._enriched_info = EnrichedInfo(self) 621 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 {...}
623 def history( 624 self, 625 period: str = "1mo", 626 interval: str = "1d", 627 start: datetime | str | None = None, 628 end: datetime | str | None = None, 629 actions: bool = False, 630 adjust: bool = True, 631 ) -> pd.DataFrame: 632 """ 633 Get historical OHLCV data. 634 635 Args: 636 period: How much data to fetch. Valid periods: 637 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. 638 Ignored if start is provided. 639 interval: Data granularity. Valid intervals: 640 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. 641 start: Start date (string or datetime). 642 end: End date (string or datetime). Defaults to today. 643 actions: If True, include Dividends and Stock Splits columns. 644 Defaults to False. 645 adjust: If True (default), return split-adjusted prices. 646 If False, return unadjusted (raw) prices. 647 648 Returns: 649 DataFrame with columns: Open, High, Low, Close, Volume. 650 If actions=True, also includes Dividends and Stock Splits columns. 651 Index is the Date. 652 653 Examples: 654 >>> stock = Ticker("THYAO") 655 >>> stock.history(period="1mo") # Last month 656 >>> stock.history(period="1y", interval="1wk") # Weekly for 1 year 657 >>> stock.history(start="2024-01-01", end="2024-06-30") # Date range 658 >>> stock.history(period="1y", actions=True) # With dividends/splits 659 >>> stock.history(period="max", adjust=False) # Raw unadjusted prices 660 """ 661 # Parse dates if strings 662 start_dt = self._parse_date(start) if start else None 663 end_dt = self._parse_date(end) if end else None 664 665 df = self._tradingview.get_history( 666 symbol=self._symbol, 667 period=period, 668 interval=interval, 669 start=start_dt, 670 end=end_dt, 671 ) 672 673 if not adjust and not df.empty: 674 df = self._unadjust_prices(df) 675 676 if actions and not df.empty: 677 df = self._add_actions_to_history(df) 678 679 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. adjust: If True (default), return split-adjusted prices. If False, return unadjusted (raw) prices.
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 stock.history(period="max", adjust=False) # Raw unadjusted prices
788 @cached_property 789 def dividends(self) -> pd.DataFrame: 790 """ 791 Get dividend history. 792 793 Returns: 794 DataFrame with dividend history: 795 - Amount: Dividend per share (TL) 796 - GrossRate: Gross dividend rate (%) 797 - NetRate: Net dividend rate (%) 798 - TotalDividend: Total dividend distributed (TL) 799 800 Examples: 801 >>> stock = Ticker("THYAO") 802 >>> stock.dividends 803 Amount GrossRate NetRate TotalDividend 804 Date 805 2025-09-02 3.442 344.20 292.57 4750000000.0 806 2025-06-16 3.442 344.20 292.57 4750000000.0 807 """ 808 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
810 @cached_property 811 def splits(self) -> pd.DataFrame: 812 """ 813 Get capital increase (split) history. 814 815 Note: Turkish market uses capital increases instead of traditional splits. 816 - RightsIssue: Paid capital increase (bedelli) 817 - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak) 818 - BonusFromDividend: Free shares from dividend (bedelsiz temettüden) 819 820 Returns: 821 DataFrame with capital increase history: 822 - Capital: New capital after increase (TL) 823 - RightsIssue: Rights issue rate (%) 824 - BonusFromCapital: Bonus from capital (%) 825 - BonusFromDividend: Bonus from dividend (%) 826 827 Examples: 828 >>> stock = Ticker("THYAO") 829 >>> stock.splits 830 Capital RightsIssue BonusFromCapital BonusFromDividend 831 Date 832 2013-06-26 1380000000.0 0.0 15.00 0.0 833 2011-07-11 1200000000.0 0.0 0.00 20.0 834 """ 835 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
837 @cached_property 838 def actions(self) -> pd.DataFrame: 839 """ 840 Get combined dividends and splits history. 841 842 Returns: 843 DataFrame with combined dividend and split actions: 844 - Dividends: Dividend per share (TL) or 0 845 - Splits: Combined split ratio (0 if no split) 846 847 Examples: 848 >>> stock = Ticker("THYAO") 849 >>> stock.actions 850 Dividends Splits 851 Date 852 2025-09-02 3.442 0.0 853 2013-06-26 0.000 15.0 854 """ 855 dividends = self.dividends 856 splits = self.splits 857 858 # Merge on index (Date) 859 if dividends.empty and splits.empty: 860 return pd.DataFrame(columns=["Dividends", "Splits"]) 861 862 # Extract relevant columns 863 div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float) 864 split_series = ( 865 splits["BonusFromCapital"] + splits["BonusFromDividend"] 866 if not splits.empty 867 else pd.Series(dtype=float) 868 ) 869 870 # Combine into single DataFrame 871 result = pd.DataFrame({"Dividends": div_series, "Splits": split_series}) 872 result = result.fillna(0) 873 result = result.sort_index(ascending=False) 874 875 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
877 def get_balance_sheet( 878 self, 879 quarterly: bool = False, 880 financial_group: str | None = None, 881 last_n: int | str | None = None, 882 ) -> pd.DataFrame: 883 """ 884 Get balance sheet data. 885 886 Args: 887 quarterly: If True, return quarterly data. If False, return annual. 888 financial_group: Financial group code. Use "UFRS" for banks, 889 "XI_29" for industrial companies. If None, defaults to XI_29. 890 last_n: Number of periods to fetch. None for default (5), int for exact 891 count (e.g. 10 = 10 annual periods), "all" for maximum available. 892 893 Returns: 894 DataFrame with balance sheet items as rows and periods as columns. 895 896 Examples: 897 >>> stock = bp.Ticker("THYAO") 898 >>> stock.get_balance_sheet() # Annual, industrial (5 periods) 899 >>> stock.get_balance_sheet(quarterly=True, last_n=20) # 20 quarters 900 901 >>> bank = bp.Ticker("AKBNK") 902 >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all") 903 """ 904 return self._get_isyatirim().get_financial_statements( 905 symbol=self._symbol, 906 statement_type="balance_sheet", 907 quarterly=quarterly, 908 financial_group=financial_group, 909 last_n=last_n, 910 )
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. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.
Returns: DataFrame with balance sheet items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_balance_sheet() # Annual, industrial (5 periods) stock.get_balance_sheet(quarterly=True, last_n=20) # 20 quarters
>>> bank = bp.Ticker("AKBNK") >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all")
912 def get_income_stmt( 913 self, 914 quarterly: bool = False, 915 financial_group: str | None = None, 916 last_n: int | str | None = None, 917 ) -> pd.DataFrame: 918 """ 919 Get income statement data. 920 921 Args: 922 quarterly: If True, return quarterly data. If False, return annual. 923 financial_group: Financial group code. Use "UFRS" for banks, 924 "XI_29" for industrial companies. If None, defaults to XI_29. 925 last_n: Number of periods to fetch. None for default (5), int for exact 926 count (e.g. 10 = 10 annual periods), "all" for maximum available. 927 928 Returns: 929 DataFrame with income statement items as rows and periods as columns. 930 931 Examples: 932 >>> stock = bp.Ticker("THYAO") 933 >>> stock.get_income_stmt() # Annual (5 periods) 934 >>> stock.get_income_stmt(quarterly=True, last_n=20) # 20 quarters 935 936 >>> bank = bp.Ticker("AKBNK") 937 >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS") 938 """ 939 return self._get_isyatirim().get_financial_statements( 940 symbol=self._symbol, 941 statement_type="income_stmt", 942 quarterly=quarterly, 943 financial_group=financial_group, 944 last_n=last_n, 945 )
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. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.
Returns: DataFrame with income statement items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_income_stmt() # Annual (5 periods) stock.get_income_stmt(quarterly=True, last_n=20) # 20 quarters
>>> bank = bp.Ticker("AKBNK") >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
947 def get_cashflow( 948 self, 949 quarterly: bool = False, 950 financial_group: str | None = None, 951 last_n: int | str | None = None, 952 ) -> pd.DataFrame: 953 """ 954 Get cash flow statement data. 955 956 Args: 957 quarterly: If True, return quarterly data. If False, return annual. 958 financial_group: Financial group code. Use "UFRS" for banks, 959 "XI_29" for industrial companies. If None, defaults to XI_29. 960 last_n: Number of periods to fetch. None for default (5), int for exact 961 count (e.g. 10 = 10 annual periods), "all" for maximum available. 962 963 Returns: 964 DataFrame with cash flow items as rows and periods as columns. 965 966 Examples: 967 >>> stock = bp.Ticker("THYAO") 968 >>> stock.get_cashflow() # Annual (5 periods) 969 >>> stock.get_cashflow(quarterly=True, last_n=20) # 20 quarters 970 971 >>> bank = bp.Ticker("AKBNK") 972 >>> bank.get_cashflow(financial_group="UFRS", last_n="all") 973 """ 974 return self._get_isyatirim().get_financial_statements( 975 symbol=self._symbol, 976 statement_type="cashflow", 977 quarterly=quarterly, 978 financial_group=financial_group, 979 last_n=last_n, 980 )
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. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.
Returns: DataFrame with cash flow items as rows and periods as columns.
Examples:
stock = bp.Ticker("THYAO") stock.get_cashflow() # Annual (5 periods) stock.get_cashflow(quarterly=True, last_n=20) # 20 quarters
>>> bank = bp.Ticker("AKBNK") >>> bank.get_cashflow(financial_group="UFRS", last_n="all")
983 @cached_property 984 def balance_sheet(self) -> pd.DataFrame: 985 """Annual balance sheet (use get_balance_sheet() for more options).""" 986 return self.get_balance_sheet(quarterly=False)
Annual balance sheet (use get_balance_sheet() for more options).
988 @cached_property 989 def quarterly_balance_sheet(self) -> pd.DataFrame: 990 """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).""" 991 return self.get_balance_sheet(quarterly=True)
Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).
993 @cached_property 994 def income_stmt(self) -> pd.DataFrame: 995 """Annual income statement (use get_income_stmt() for more options).""" 996 return self.get_income_stmt(quarterly=False)
Annual income statement (use get_income_stmt() for more options).
998 @cached_property 999 def quarterly_income_stmt(self) -> pd.DataFrame: 1000 """Quarterly income statement (use get_income_stmt(quarterly=True) for more options).""" 1001 return self.get_income_stmt(quarterly=True)
Quarterly income statement (use get_income_stmt(quarterly=True) for more options).
1003 @cached_property 1004 def cashflow(self) -> pd.DataFrame: 1005 """Annual cash flow (use get_cashflow() for more options).""" 1006 return self.get_cashflow(quarterly=False)
Annual cash flow (use get_cashflow() for more options).
1008 @cached_property 1009 def quarterly_cashflow(self) -> pd.DataFrame: 1010 """Quarterly cash flow (use get_cashflow(quarterly=True) for more options).""" 1011 return self.get_cashflow(quarterly=True)
Quarterly cash flow (use get_cashflow(quarterly=True) for more options).
1034 def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame: 1035 """ 1036 Get trailing twelve months (TTM) income statement. 1037 1038 Calculates TTM by summing the last 4 quarters of income statement data. 1039 1040 Args: 1041 financial_group: Financial group code. Use "UFRS" for banks, 1042 "XI_29" for industrial companies. If None, defaults to XI_29. 1043 1044 Returns: 1045 DataFrame with TTM column containing summed values for each line item. 1046 1047 Examples: 1048 >>> stock = bp.Ticker("THYAO") 1049 >>> stock.get_ttm_income_stmt() 1050 1051 >>> bank = bp.Ticker("AKBNK") 1052 >>> bank.get_ttm_income_stmt(financial_group="UFRS") 1053 """ 1054 quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group) 1055 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")
1057 def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame: 1058 """ 1059 Get trailing twelve months (TTM) cash flow statement. 1060 1061 Calculates TTM by summing the last 4 quarters of cash flow data. 1062 1063 Args: 1064 financial_group: Financial group code. Use "UFRS" for banks, 1065 "XI_29" for industrial companies. If None, defaults to XI_29. 1066 1067 Returns: 1068 DataFrame with TTM column containing summed values for each line item. 1069 1070 Examples: 1071 >>> stock = bp.Ticker("THYAO") 1072 >>> stock.get_ttm_cashflow() 1073 1074 >>> bank = bp.Ticker("AKBNK") 1075 >>> bank.get_ttm_cashflow(financial_group="UFRS") 1076 """ 1077 quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group) 1078 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")
1081 @cached_property 1082 def ttm_income_stmt(self) -> pd.DataFrame: 1083 """TTM income statement (use get_ttm_income_stmt() for banks).""" 1084 return self.get_ttm_income_stmt()
TTM income statement (use get_ttm_income_stmt() for banks).
1086 @cached_property 1087 def ttm_cashflow(self) -> pd.DataFrame: 1088 """TTM cash flow (use get_ttm_cashflow() for banks).""" 1089 return self.get_ttm_cashflow()
TTM cash flow (use get_ttm_cashflow() for banks).
1091 @cached_property 1092 def major_holders(self) -> pd.DataFrame: 1093 """ 1094 Get major shareholders (ortaklık yapısı). 1095 1096 Returns: 1097 DataFrame with shareholder names and percentages: 1098 - Index: Holder name 1099 - Percentage: Ownership percentage (%) 1100 1101 Examples: 1102 >>> stock = Ticker("THYAO") 1103 >>> stock.major_holders 1104 Percentage 1105 Holder 1106 Diğer 50.88 1107 Türkiye Varlık Fonu 49.12 1108 """ 1109 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
1111 @cached_property 1112 def recommendations(self) -> dict: 1113 """ 1114 Get analyst recommendations and target price. 1115 1116 Returns: 1117 Dictionary with: 1118 - recommendation: Buy/Hold/Sell (AL/TUT/SAT) 1119 - target_price: Analyst target price (TL) 1120 - upside_potential: Expected upside (%) 1121 1122 Examples: 1123 >>> stock = Ticker("THYAO") 1124 >>> stock.recommendations 1125 {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01} 1126 """ 1127 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}
1129 @cached_property 1130 def recommendations_summary(self) -> dict[str, int]: 1131 """ 1132 Get analyst recommendation summary with buy/hold/sell counts. 1133 1134 Aggregates individual analyst recommendations from hedeffiyat.com.tr 1135 into yfinance-compatible categories. 1136 1137 Returns: 1138 Dictionary with counts: 1139 - strongBuy: Strong buy recommendations 1140 - buy: Buy recommendations (includes "Endeks Üstü Getiri") 1141 - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") 1142 - sell: Sell recommendations (includes "Endeks Altı Getiri") 1143 - strongSell: Strong sell recommendations 1144 1145 Examples: 1146 >>> stock = Ticker("THYAO") 1147 >>> stock.recommendations_summary 1148 {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0} 1149 """ 1150 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}
1152 @cached_property 1153 def news(self) -> pd.DataFrame: 1154 """ 1155 Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock. 1156 1157 Fetches directly from KAP - the official disclosure platform for 1158 publicly traded companies in Turkey. 1159 1160 Returns: 1161 DataFrame with columns: 1162 - Date: Disclosure date and time 1163 - Title: Disclosure headline 1164 - URL: Link to full disclosure on KAP 1165 1166 Examples: 1167 >>> stock = Ticker("THYAO") 1168 >>> stock.news 1169 Date Title URL 1170 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1171 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656 1172 """ 1173 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
1175 def get_news_content(self, disclosure_id: int | str) -> str | None: 1176 """ 1177 Get full HTML content of a KAP disclosure by ID. 1178 1179 Args: 1180 disclosure_id: KAP disclosure ID from news DataFrame URL. 1181 1182 Returns: 1183 Raw HTML content or None if failed. 1184 1185 Examples: 1186 >>> stock = Ticker("THYAO") 1187 >>> html = stock.get_news_content(1530826) 1188 """ 1189 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)
1191 @cached_property 1192 def calendar(self) -> pd.DataFrame: 1193 """ 1194 Get expected disclosure calendar for the stock from KAP. 1195 1196 Returns upcoming expected disclosures like financial reports, 1197 annual reports, sustainability reports, and corporate governance reports. 1198 1199 Returns: 1200 DataFrame with columns: 1201 - StartDate: Expected disclosure window start 1202 - EndDate: Expected disclosure window end 1203 - Subject: Type of disclosure (e.g., "Finansal Rapor") 1204 - Period: Report period (e.g., "Yıllık", "3 Aylık") 1205 - Year: Fiscal year 1206 1207 Examples: 1208 >>> stock = Ticker("THYAO") 1209 >>> stock.calendar 1210 StartDate EndDate Subject Period Year 1211 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1212 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 1213 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026 1214 """ 1215 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
1217 @cached_property 1218 def isin(self) -> str | None: 1219 """ 1220 Get ISIN (International Securities Identification Number) code. 1221 1222 ISIN is a 12-character alphanumeric code that uniquely identifies 1223 a security, standardized by ISO 6166. 1224 1225 Returns: 1226 ISIN code string (e.g., "TRATHYAO91M5") or None if not found. 1227 1228 Examples: 1229 >>> stock = Ticker("THYAO") 1230 >>> stock.isin 1231 'TRATHYAO91M5' 1232 """ 1233 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'
1235 @cached_property 1236 def analyst_price_targets(self) -> dict[str, float | int | None]: 1237 """ 1238 Get analyst price target data from hedeffiyat.com.tr. 1239 1240 Returns aggregated price target information from multiple analysts. 1241 1242 Returns: 1243 Dictionary with: 1244 - current: Current stock price 1245 - low: Lowest analyst target price 1246 - high: Highest analyst target price 1247 - mean: Average target price 1248 - median: Median target price 1249 - numberOfAnalysts: Number of analysts covering the stock 1250 1251 Examples: 1252 >>> stock = Ticker("THYAO") 1253 >>> stock.analyst_price_targets 1254 {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 1255 'median': 465.0, 'numberOfAnalysts': 19} 1256 """ 1257 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}
1259 @property 1260 def etf_holders(self) -> pd.DataFrame: 1261 """ 1262 Get international ETFs that hold this stock. 1263 1264 Returns data from TradingView showing which ETFs hold this stock, 1265 including position value, weight, and ETF characteristics. 1266 1267 Returns: 1268 DataFrame with ETF holder information: 1269 - symbol: ETF ticker symbol 1270 - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) 1271 - name: ETF full name 1272 - market_cap_usd: Position value in USD 1273 - holding_weight_pct: Weight percentage (0.09 = 0.09%) 1274 - issuer: ETF issuer (BlackRock, Vanguard, etc.) 1275 - management: Management style (Passive/Active) 1276 - focus: Investment focus (Total Market, Emerging Markets, etc.) 1277 - expense_ratio: Expense ratio (0.09 = 0.09%) 1278 - aum_usd: Total assets under management (USD) 1279 - price: Current ETF price 1280 - change_pct: Change percentage 1281 1282 Examples: 1283 >>> stock = Ticker("ASELS") 1284 >>> holders = stock.etf_holders 1285 >>> holders[['symbol', 'name', 'holding_weight_pct']].head() 1286 symbol name holding_weight_pct 1287 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1288 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000 1289 1290 >>> print(f"Total ETFs: {len(holders)}") 1291 Total ETFs: 118 1292 """ 1293 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
1295 @cached_property 1296 def earnings_dates(self) -> pd.DataFrame: 1297 """ 1298 Get upcoming earnings announcement dates. 1299 1300 Derived from KAP calendar, showing expected financial report dates. 1301 Compatible with yfinance earnings_dates format. 1302 1303 Returns: 1304 DataFrame with index as Earnings Date and columns: 1305 - EPS Estimate: Always None (not available for BIST) 1306 - Reported EPS: Always None (not available for BIST) 1307 - Surprise (%): Always None (not available for BIST) 1308 1309 Examples: 1310 >>> stock = Ticker("THYAO") 1311 >>> stock.earnings_dates 1312 EPS Estimate Reported EPS Surprise(%) 1313 Earnings Date 1314 2026-03-11 None None None 1315 2026-05-11 None None None 1316 """ 1317 cal = self.calendar 1318 if cal.empty: 1319 return pd.DataFrame( 1320 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1321 ) 1322 1323 # Filter for financial reports only 1324 financial_reports = cal[ 1325 cal["Subject"].str.contains("Finansal Rapor", case=False, na=False) 1326 ] 1327 1328 if financial_reports.empty: 1329 return pd.DataFrame( 1330 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1331 ) 1332 1333 # Use EndDate as the earnings date (latest expected date) 1334 earnings_dates = [] 1335 for _, row in financial_reports.iterrows(): 1336 end_date = row.get("EndDate", "") 1337 if end_date: 1338 try: 1339 # Parse Turkish date format (DD.MM.YYYY) 1340 parsed = datetime.strptime(end_date, "%d.%m.%Y") 1341 earnings_dates.append(parsed) 1342 except ValueError: 1343 continue 1344 1345 if not earnings_dates: 1346 return pd.DataFrame( 1347 columns=["EPS Estimate", "Reported EPS", "Surprise(%)"] 1348 ) 1349 1350 result = pd.DataFrame( 1351 { 1352 "EPS Estimate": [None] * len(earnings_dates), 1353 "Reported EPS": [None] * len(earnings_dates), 1354 "Surprise(%)": [None] * len(earnings_dates), 1355 }, 1356 index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"), 1357 ) 1358 result = result.sort_index() 1359 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.
74class FX(TechnicalMixin, TwitterMixin): 75 """ 76 A yfinance-like interface for forex and commodity data. 77 78 Supported assets: 79 - Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD (+ 58 more via canlidoviz) 80 - Precious Metals: gram-altin, gumus, ons-altin, gram-platin, XAG-USD, XPT-USD, XPD-USD 81 - Energy: BRENT 82 83 Examples: 84 >>> import borsapy as bp 85 >>> usd = bp.FX("USD") 86 >>> usd.current 87 {'symbol': 'USD', 'last': 34.85, ...} 88 >>> usd.history(period="1mo") 89 Open High Low Close 90 Date 91 2024-12-01 34.50 34.80 34.40 34.75 92 ... 93 94 >>> gold = bp.FX("gram-altin") 95 >>> gold.current 96 {'symbol': 'gram-altin', 'last': 2850.50, ...} 97 """ 98 99 def __init__(self, asset: str): 100 """ 101 Initialize an FX object. 102 103 Args: 104 asset: Asset code (USD, EUR, gram-altin, BRENT, etc.) 105 """ 106 self._asset = asset 107 self._canlidoviz = get_canlidoviz_provider() 108 self._dovizcom = get_dovizcom_provider() 109 self._tradingview = get_tradingview_provider() 110 self._current_cache: dict[str, Any] | None = None 111 112 def _get_tweet_query(self) -> str: 113 return _build_fx_query(self._asset) 114 115 def _use_canlidoviz(self) -> bool: 116 """Check if canlidoviz should be used for this asset.""" 117 asset_upper = self._asset.upper() 118 # Currencies 119 if asset_upper in self._canlidoviz.CURRENCY_IDS: 120 return True 121 # Metals supported by canlidoviz (TRY prices) 122 if self._asset in self._canlidoviz.METAL_IDS: 123 return True 124 # Energy supported by canlidoviz (USD prices) 125 if asset_upper in self._canlidoviz.ENERGY_IDS: 126 return True 127 # Commodities supported by canlidoviz (USD prices) 128 if asset_upper in self._canlidoviz.COMMODITY_IDS: 129 return True 130 return False 131 132 def _get_tradingview_symbol(self) -> tuple[str, str] | None: 133 """Get TradingView exchange and symbol for this asset. 134 135 Returns: 136 Tuple of (exchange, symbol) or None if not supported. 137 """ 138 asset_upper = self._asset.upper() 139 140 # Check currency map first 141 if asset_upper in TV_CURRENCY_MAP: 142 return TV_CURRENCY_MAP[asset_upper] 143 144 # Check commodity map 145 if self._asset in TV_COMMODITY_MAP: 146 return TV_COMMODITY_MAP[self._asset] 147 if asset_upper in TV_COMMODITY_MAP: 148 return TV_COMMODITY_MAP[asset_upper] 149 150 return None 151 152 @property 153 def asset(self) -> str: 154 """Return the asset code.""" 155 return self._asset 156 157 @property 158 def symbol(self) -> str: 159 """Return the asset code (alias for asset).""" 160 return self._asset 161 162 @property 163 def current(self) -> dict[str, Any]: 164 """ 165 Get current price information. 166 167 Returns: 168 Dictionary with current market data: 169 - symbol: Asset code 170 - last: Last price 171 - open: Opening price 172 - high: Day high 173 - low: Day low 174 - update_time: Last update timestamp 175 """ 176 if self._current_cache is None: 177 if self._use_canlidoviz(): 178 self._current_cache = self._canlidoviz.get_current(self._asset) 179 else: 180 try: 181 self._current_cache = self._dovizcom.get_current(self._asset) 182 except Exception: 183 # Fallback to bank_rates for currencies not supported by APIs 184 self._current_cache = self._current_from_bank_rates() 185 return self._current_cache 186 187 def _current_from_bank_rates(self) -> dict[str, Any]: 188 """Calculate current price from bank rates as fallback.""" 189 from datetime import datetime 190 191 rates = self._dovizcom.get_bank_rates(self._asset) 192 if rates.empty: 193 raise ValueError(f"No data available for {self._asset}") 194 195 # Calculate average mid price from all banks 196 mids = (rates["buy"] + rates["sell"]) / 2 197 avg_mid = float(mids.mean()) 198 199 return { 200 "symbol": self._asset, 201 "last": avg_mid, 202 "open": avg_mid, 203 "high": float(rates["sell"].max()), 204 "low": float(rates["buy"].min()), 205 "update_time": datetime.now(), 206 "source": "bank_rates_avg", 207 } 208 209 @property 210 def info(self) -> dict[str, Any]: 211 """Alias for current property (yfinance compatibility).""" 212 return self.current 213 214 @property 215 def bank_rates(self) -> pd.DataFrame: 216 """ 217 Get exchange rates from all banks. 218 219 Returns: 220 DataFrame with columns: bank, bank_name, currency, buy, sell, spread 221 222 Examples: 223 >>> usd = FX("USD") 224 >>> usd.bank_rates 225 bank bank_name currency buy sell spread 226 0 akbank Akbank USD 41.6610 44.1610 5.99 227 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 228 ... 229 """ 230 return self._dovizcom.get_bank_rates(self._asset) 231 232 def bank_rate(self, bank: str) -> dict[str, Any]: 233 """ 234 Get exchange rate from a specific bank. 235 236 Args: 237 bank: Bank code (akbank, garanti, isbank, ziraat, etc.) 238 239 Returns: 240 Dictionary with keys: bank, currency, buy, sell, spread 241 242 Examples: 243 >>> usd = FX("USD") 244 >>> usd.bank_rate("akbank") 245 {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99} 246 """ 247 return self._dovizcom.get_bank_rates(self._asset, bank=bank) 248 249 @staticmethod 250 def banks() -> list[str]: 251 """ 252 Get list of supported banks. 253 254 Returns: 255 List of bank codes. 256 257 Examples: 258 >>> FX.banks() 259 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 260 """ 261 from borsapy._providers.dovizcom import get_dovizcom_provider 262 263 return get_dovizcom_provider().get_banks() 264 265 @property 266 def institution_rates(self) -> pd.DataFrame: 267 """ 268 Get precious metal rates from all institutions (kuyumcular, bankalar). 269 270 Only available for precious metals: gram-altin, gram-gumus, ons-altin, 271 gram-platin 272 273 Returns: 274 DataFrame with columns: institution, institution_name, asset, buy, sell, spread 275 276 Examples: 277 >>> gold = FX("gram-altin") 278 >>> gold.institution_rates 279 institution institution_name asset buy sell spread 280 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 281 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 282 ... 283 """ 284 return self._dovizcom.get_metal_institution_rates(self._asset) 285 286 def institution_rate(self, institution: str) -> dict[str, Any]: 287 """ 288 Get precious metal rate from a specific institution. 289 290 Args: 291 institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.) 292 293 Returns: 294 Dictionary with keys: institution, institution_name, asset, buy, sell, spread 295 296 Examples: 297 >>> gold = FX("gram-altin") 298 >>> gold.institution_rate("akbank") 299 {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 300 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32} 301 """ 302 return self._dovizcom.get_metal_institution_rates(self._asset, institution=institution) 303 304 @staticmethod 305 def metal_institutions() -> list[str]: 306 """ 307 Get list of supported precious metal assets for institution rates. 308 309 Returns: 310 List of asset codes that support institution_rates. 311 312 Examples: 313 >>> FX.metal_institutions() 314 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 315 """ 316 from borsapy._providers.dovizcom import get_dovizcom_provider 317 318 return get_dovizcom_provider().get_metal_institutions() 319 320 def history( 321 self, 322 period: str = "1mo", 323 interval: str = "1d", 324 start: datetime | str | None = None, 325 end: datetime | str | None = None, 326 ) -> pd.DataFrame: 327 """ 328 Get historical OHLC data. 329 330 Args: 331 period: How much data to fetch. Valid periods: 332 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. 333 Ignored if start is provided. 334 interval: Data interval. Valid intervals: 335 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. 336 Note: Intraday intervals (1m-4h) use TradingView. 337 Daily and above use canlidoviz/dovizcom. 338 start: Start date (string or datetime). 339 end: End date (string or datetime). Defaults to today. 340 341 Returns: 342 DataFrame with columns: Open, High, Low, Close, Volume. 343 Index is the Date. 344 345 Examples: 346 >>> fx = FX("USD") 347 >>> fx.history(period="1mo") # Last month daily 348 >>> fx.history(period="1d", interval="1m") # Today's minute data 349 >>> fx.history(period="5d", interval="1h") # 5 days hourly 350 >>> fx.history(start="2024-01-01", end="2024-06-30") # Date range 351 """ 352 start_dt = self._parse_date(start) if start else None 353 end_dt = self._parse_date(end) if end else None 354 355 # Use TradingView for intraday intervals 356 intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h") 357 if interval in intraday_intervals: 358 tv_info = self._get_tradingview_symbol() 359 if tv_info is None: 360 raise ValueError( 361 f"Intraday data not available for {self._asset}. " 362 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}" 363 ) 364 365 exchange, symbol = tv_info 366 return self._tradingview.get_history( 367 symbol=symbol, 368 period=period, 369 interval=interval, 370 start=start_dt, 371 end=end_dt, 372 exchange=exchange, 373 ) 374 375 # Use canlidoviz/dovizcom for daily and above 376 if self._use_canlidoviz(): 377 return self._canlidoviz.get_history( 378 asset=self._asset, 379 period=period, 380 start=start_dt, 381 end=end_dt, 382 ) 383 else: 384 return self._dovizcom.get_history( 385 asset=self._asset, 386 period=period, 387 start=start_dt, 388 end=end_dt, 389 ) 390 391 def institution_history( 392 self, 393 institution: str, 394 period: str = "1mo", 395 start: datetime | str | None = None, 396 end: datetime | str | None = None, 397 ) -> pd.DataFrame: 398 """ 399 Get historical OHLC data from a specific institution. 400 401 Supports both precious metals and currencies. 402 403 Args: 404 institution: Institution slug (akbank, kapalicarsi, harem, etc.) 405 period: How much data to fetch. Valid periods: 406 1d, 5d, 1mo, 3mo, 6mo, 1y. 407 Ignored if start is provided. 408 start: Start date (string or datetime). 409 end: End date (string or datetime). Defaults to today. 410 411 Returns: 412 DataFrame with columns: Open, High, Low, Close. 413 Index is the Date. 414 Note: Banks typically return only Close values (Open/High/Low = 0). 415 416 Examples: 417 >>> # Metal history 418 >>> gold = FX("gram-altin") 419 >>> gold.institution_history("akbank", period="1mo") 420 >>> gold.institution_history("kapalicarsi", start="2024-01-01") 421 422 >>> # Currency history 423 >>> usd = FX("USD") 424 >>> usd.institution_history("akbank", period="1mo") 425 >>> usd.institution_history("garanti-bbva", period="5d") 426 """ 427 start_dt = self._parse_date(start) if start else None 428 end_dt = self._parse_date(end) if end else None 429 430 # Use canlidoviz for currencies and precious metals (bank-specific rates) 431 asset_upper = self._asset.upper() 432 use_canlidoviz = ( 433 asset_upper in self._canlidoviz.CURRENCY_IDS 434 or self._asset in ("gram-altin", "gumus", "gram-platin") 435 ) 436 437 if use_canlidoviz: 438 # Check if canlidoviz has bank ID for this asset 439 try: 440 return self._canlidoviz.get_history( 441 asset=self._asset, 442 period=period, 443 start=start_dt, 444 end=end_dt, 445 institution=institution, 446 ) 447 except Exception: 448 # Fall back to dovizcom if canlidoviz doesn't support this bank 449 pass 450 451 # Use dovizcom for other metals and unsupported banks 452 return self._dovizcom.get_institution_history( 453 asset=self._asset, 454 institution=institution, 455 period=period, 456 start=start_dt, 457 end=end_dt, 458 ) 459 460 def _parse_date(self, date: str | datetime) -> datetime: 461 """Parse a date string to datetime.""" 462 if isinstance(date, datetime): 463 return date 464 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 465 try: 466 return datetime.strptime(date, fmt) 467 except ValueError: 468 continue 469 raise ValueError(f"Could not parse date: {date}") 470 471 def _get_ta_symbol_info(self) -> tuple[str, str]: 472 """Get TradingView symbol and screener for TA signals. 473 474 Returns: 475 Tuple of (tv_symbol, screener) for TradingView Scanner API. 476 477 Raises: 478 NotImplementedError: If TA signals not available for this asset. 479 """ 480 tv_info = self._get_tradingview_symbol() 481 if tv_info is None: 482 raise NotImplementedError( 483 f"TA signals not available for {self._asset}. " 484 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}. " 485 f"Supported commodities: {list(TV_COMMODITY_MAP.keys())}." 486 ) 487 exchange, symbol = tv_info 488 return (f"{exchange}:{symbol}", "forex") 489 490 def __repr__(self) -> str: 491 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, ...}
99 def __init__(self, asset: str): 100 """ 101 Initialize an FX object. 102 103 Args: 104 asset: Asset code (USD, EUR, gram-altin, BRENT, etc.) 105 """ 106 self._asset = asset 107 self._canlidoviz = get_canlidoviz_provider() 108 self._dovizcom = get_dovizcom_provider() 109 self._tradingview = get_tradingview_provider() 110 self._current_cache: dict[str, Any] | None = None
Initialize an FX object.
Args: asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)
157 @property 158 def symbol(self) -> str: 159 """Return the asset code (alias for asset).""" 160 return self._asset
Return the asset code (alias for asset).
162 @property 163 def current(self) -> dict[str, Any]: 164 """ 165 Get current price information. 166 167 Returns: 168 Dictionary with current market data: 169 - symbol: Asset code 170 - last: Last price 171 - open: Opening price 172 - high: Day high 173 - low: Day low 174 - update_time: Last update timestamp 175 """ 176 if self._current_cache is None: 177 if self._use_canlidoviz(): 178 self._current_cache = self._canlidoviz.get_current(self._asset) 179 else: 180 try: 181 self._current_cache = self._dovizcom.get_current(self._asset) 182 except Exception: 183 # Fallback to bank_rates for currencies not supported by APIs 184 self._current_cache = self._current_from_bank_rates() 185 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
209 @property 210 def info(self) -> dict[str, Any]: 211 """Alias for current property (yfinance compatibility).""" 212 return self.current
Alias for current property (yfinance compatibility).
214 @property 215 def bank_rates(self) -> pd.DataFrame: 216 """ 217 Get exchange rates from all banks. 218 219 Returns: 220 DataFrame with columns: bank, bank_name, currency, buy, sell, spread 221 222 Examples: 223 >>> usd = FX("USD") 224 >>> usd.bank_rates 225 bank bank_name currency buy sell spread 226 0 akbank Akbank USD 41.6610 44.1610 5.99 227 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 228 ... 229 """ 230 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 ...
232 def bank_rate(self, bank: str) -> dict[str, Any]: 233 """ 234 Get exchange rate from a specific bank. 235 236 Args: 237 bank: Bank code (akbank, garanti, isbank, ziraat, etc.) 238 239 Returns: 240 Dictionary with keys: bank, currency, buy, sell, spread 241 242 Examples: 243 >>> usd = FX("USD") 244 >>> usd.bank_rate("akbank") 245 {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99} 246 """ 247 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}
249 @staticmethod 250 def banks() -> list[str]: 251 """ 252 Get list of supported banks. 253 254 Returns: 255 List of bank codes. 256 257 Examples: 258 >>> FX.banks() 259 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 260 """ 261 from borsapy._providers.dovizcom import get_dovizcom_provider 262 263 return get_dovizcom_provider().get_banks()
Get list of supported banks.
Returns: List of bank codes.
Examples:
FX.banks() ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
265 @property 266 def institution_rates(self) -> pd.DataFrame: 267 """ 268 Get precious metal rates from all institutions (kuyumcular, bankalar). 269 270 Only available for precious metals: gram-altin, gram-gumus, ons-altin, 271 gram-platin 272 273 Returns: 274 DataFrame with columns: institution, institution_name, asset, buy, sell, spread 275 276 Examples: 277 >>> gold = FX("gram-altin") 278 >>> gold.institution_rates 279 institution institution_name asset buy sell spread 280 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 281 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 282 ... 283 """ 284 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 ...
286 def institution_rate(self, institution: str) -> dict[str, Any]: 287 """ 288 Get precious metal rate from a specific institution. 289 290 Args: 291 institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.) 292 293 Returns: 294 Dictionary with keys: institution, institution_name, asset, buy, sell, spread 295 296 Examples: 297 >>> gold = FX("gram-altin") 298 >>> gold.institution_rate("akbank") 299 {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 300 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32} 301 """ 302 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}
304 @staticmethod 305 def metal_institutions() -> list[str]: 306 """ 307 Get list of supported precious metal assets for institution rates. 308 309 Returns: 310 List of asset codes that support institution_rates. 311 312 Examples: 313 >>> FX.metal_institutions() 314 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 315 """ 316 from borsapy._providers.dovizcom import get_dovizcom_provider 317 318 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']
320 def history( 321 self, 322 period: str = "1mo", 323 interval: str = "1d", 324 start: datetime | str | None = None, 325 end: datetime | str | None = None, 326 ) -> pd.DataFrame: 327 """ 328 Get historical OHLC data. 329 330 Args: 331 period: How much data to fetch. Valid periods: 332 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. 333 Ignored if start is provided. 334 interval: Data interval. Valid intervals: 335 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. 336 Note: Intraday intervals (1m-4h) use TradingView. 337 Daily and above use canlidoviz/dovizcom. 338 start: Start date (string or datetime). 339 end: End date (string or datetime). Defaults to today. 340 341 Returns: 342 DataFrame with columns: Open, High, Low, Close, Volume. 343 Index is the Date. 344 345 Examples: 346 >>> fx = FX("USD") 347 >>> fx.history(period="1mo") # Last month daily 348 >>> fx.history(period="1d", interval="1m") # Today's minute data 349 >>> fx.history(period="5d", interval="1h") # 5 days hourly 350 >>> fx.history(start="2024-01-01", end="2024-06-30") # Date range 351 """ 352 start_dt = self._parse_date(start) if start else None 353 end_dt = self._parse_date(end) if end else None 354 355 # Use TradingView for intraday intervals 356 intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h") 357 if interval in intraday_intervals: 358 tv_info = self._get_tradingview_symbol() 359 if tv_info is None: 360 raise ValueError( 361 f"Intraday data not available for {self._asset}. " 362 f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}" 363 ) 364 365 exchange, symbol = tv_info 366 return self._tradingview.get_history( 367 symbol=symbol, 368 period=period, 369 interval=interval, 370 start=start_dt, 371 end=end_dt, 372 exchange=exchange, 373 ) 374 375 # Use canlidoviz/dovizcom for daily and above 376 if self._use_canlidoviz(): 377 return self._canlidoviz.get_history( 378 asset=self._asset, 379 period=period, 380 start=start_dt, 381 end=end_dt, 382 ) 383 else: 384 return self._dovizcom.get_history( 385 asset=self._asset, 386 period=period, 387 start=start_dt, 388 end=end_dt, 389 )
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
391 def institution_history( 392 self, 393 institution: str, 394 period: str = "1mo", 395 start: datetime | str | None = None, 396 end: datetime | str | None = None, 397 ) -> pd.DataFrame: 398 """ 399 Get historical OHLC data from a specific institution. 400 401 Supports both precious metals and currencies. 402 403 Args: 404 institution: Institution slug (akbank, kapalicarsi, harem, etc.) 405 period: How much data to fetch. Valid periods: 406 1d, 5d, 1mo, 3mo, 6mo, 1y. 407 Ignored if start is provided. 408 start: Start date (string or datetime). 409 end: End date (string or datetime). Defaults to today. 410 411 Returns: 412 DataFrame with columns: Open, High, Low, Close. 413 Index is the Date. 414 Note: Banks typically return only Close values (Open/High/Low = 0). 415 416 Examples: 417 >>> # Metal history 418 >>> gold = FX("gram-altin") 419 >>> gold.institution_history("akbank", period="1mo") 420 >>> gold.institution_history("kapalicarsi", start="2024-01-01") 421 422 >>> # Currency history 423 >>> usd = FX("USD") 424 >>> usd.institution_history("akbank", period="1mo") 425 >>> usd.institution_history("garanti-bbva", period="5d") 426 """ 427 start_dt = self._parse_date(start) if start else None 428 end_dt = self._parse_date(end) if end else None 429 430 # Use canlidoviz for currencies and precious metals (bank-specific rates) 431 asset_upper = self._asset.upper() 432 use_canlidoviz = ( 433 asset_upper in self._canlidoviz.CURRENCY_IDS 434 or self._asset in ("gram-altin", "gumus", "gram-platin") 435 ) 436 437 if use_canlidoviz: 438 # Check if canlidoviz has bank ID for this asset 439 try: 440 return self._canlidoviz.get_history( 441 asset=self._asset, 442 period=period, 443 start=start_dt, 444 end=end_dt, 445 institution=institution, 446 ) 447 except Exception: 448 # Fall back to dovizcom if canlidoviz doesn't support this bank 449 pass 450 451 # Use dovizcom for other metals and unsupported banks 452 return self._dovizcom.get_institution_history( 453 asset=self._asset, 454 institution=institution, 455 period=period, 456 start=start_dt, 457 end=end_dt, 458 )
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")
14class Crypto(TechnicalMixin, TwitterMixin): 15 """ 16 A yfinance-like interface for cryptocurrency data from BtcTurk. 17 18 Examples: 19 >>> import borsapy as bp 20 >>> btc = bp.Crypto("BTCTRY") 21 >>> btc.current 22 {'symbol': 'BTCTRY', 'last': 3500000.0, ...} 23 >>> btc.history(period="1mo") 24 Open High Low Close Volume 25 Date 26 2024-12-01 3400000.0 3550000.0 3380000.0 3500000.0 1234.5678 27 ... 28 29 >>> eth = bp.Crypto("ETHTRY") 30 >>> eth.current['last'] 31 125000.0 32 """ 33 34 def __init__(self, pair: str): 35 """ 36 Initialize a Crypto object. 37 38 Args: 39 pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). 40 Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY 41 """ 42 self._pair = pair.upper() 43 self._provider = get_btcturk_provider() 44 self._current_cache: dict[str, Any] | None = None 45 46 def _get_tweet_query(self) -> str: 47 return _build_crypto_query(self._pair) 48 49 @property 50 def pair(self) -> str: 51 """Return the trading pair.""" 52 return self._pair 53 54 @property 55 def symbol(self) -> str: 56 """Return the trading pair (alias).""" 57 return self._pair 58 59 @property 60 def current(self) -> dict[str, Any]: 61 """ 62 Get current ticker information. 63 64 Returns: 65 Dictionary with current market data: 66 - symbol: Trading pair 67 - last: Last traded price 68 - open: Opening price 69 - high: 24h high 70 - low: 24h low 71 - bid: Best bid price 72 - ask: Best ask price 73 - volume: 24h volume 74 - change: Price change 75 - change_percent: Percent change 76 """ 77 if self._current_cache is None: 78 self._current_cache = self._provider.get_ticker(self._pair) 79 return self._current_cache 80 81 @property 82 def info(self) -> dict[str, Any]: 83 """Alias for current property (yfinance compatibility).""" 84 return self.current 85 86 def history( 87 self, 88 period: str = "1mo", 89 interval: str = "1d", 90 start: datetime | str | None = None, 91 end: datetime | str | None = None, 92 ) -> pd.DataFrame: 93 """ 94 Get historical OHLCV data. 95 96 Args: 97 period: How much data to fetch. Valid periods: 98 1d, 5d, 1mo, 3mo, 6mo, 1y. 99 Ignored if start is provided. 100 interval: Data granularity. Valid intervals: 101 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. 102 start: Start date (string or datetime). 103 end: End date (string or datetime). Defaults to now. 104 105 Returns: 106 DataFrame with columns: Open, High, Low, Close, Volume. 107 Index is the Date. 108 109 Examples: 110 >>> crypto = Crypto("BTCTRY") 111 >>> crypto.history(period="1mo") # Last month 112 >>> crypto.history(period="1y", interval="1wk") # Weekly for 1 year 113 >>> crypto.history(start="2024-01-01", end="2024-06-30") # Date range 114 """ 115 start_dt = self._parse_date(start) if start else None 116 end_dt = self._parse_date(end) if end else None 117 118 return self._provider.get_history( 119 pair=self._pair, 120 period=period, 121 interval=interval, 122 start=start_dt, 123 end=end_dt, 124 ) 125 126 def _parse_date(self, date: str | datetime) -> datetime: 127 """Parse a date string to datetime.""" 128 if isinstance(date, datetime): 129 return date 130 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 131 try: 132 return datetime.strptime(date, fmt) 133 except ValueError: 134 continue 135 raise ValueError(f"Could not parse date: {date}") 136 137 def _get_ta_symbol_info(self) -> tuple[str, str]: 138 """Get TradingView symbol and screener for TA signals. 139 140 Maps BtcTurk pairs to Binance USDT pairs for better TradingView coverage. 141 142 Returns: 143 Tuple of (tv_symbol, screener) for TradingView Scanner API. 144 """ 145 # Extract base currency from pair (e.g., "BTCTRY" -> "BTC") 146 base = self._pair.replace("TRY", "").replace("USDT", "") 147 # Use Binance USDT pair for better TradingView coverage 148 return (f"BINANCE:{base}USDT", "crypto") 149 150 def __repr__(self) -> str: 151 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
34 def __init__(self, pair: str): 35 """ 36 Initialize a Crypto object. 37 38 Args: 39 pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). 40 Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY 41 """ 42 self._pair = pair.upper() 43 self._provider = get_btcturk_provider() 44 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
54 @property 55 def symbol(self) -> str: 56 """Return the trading pair (alias).""" 57 return self._pair
Return the trading pair (alias).
59 @property 60 def current(self) -> dict[str, Any]: 61 """ 62 Get current ticker information. 63 64 Returns: 65 Dictionary with current market data: 66 - symbol: Trading pair 67 - last: Last traded price 68 - open: Opening price 69 - high: 24h high 70 - low: 24h low 71 - bid: Best bid price 72 - ask: Best ask price 73 - volume: 24h volume 74 - change: Price change 75 - change_percent: Percent change 76 """ 77 if self._current_cache is None: 78 self._current_cache = self._provider.get_ticker(self._pair) 79 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
81 @property 82 def info(self) -> dict[str, Any]: 83 """Alias for current property (yfinance compatibility).""" 84 return self.current
Alias for current property (yfinance compatibility).
86 def history( 87 self, 88 period: str = "1mo", 89 interval: str = "1d", 90 start: datetime | str | None = None, 91 end: datetime | str | None = None, 92 ) -> pd.DataFrame: 93 """ 94 Get historical OHLCV data. 95 96 Args: 97 period: How much data to fetch. Valid periods: 98 1d, 5d, 1mo, 3mo, 6mo, 1y. 99 Ignored if start is provided. 100 interval: Data granularity. Valid intervals: 101 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. 102 start: Start date (string or datetime). 103 end: End date (string or datetime). Defaults to now. 104 105 Returns: 106 DataFrame with columns: Open, High, Low, Close, Volume. 107 Index is the Date. 108 109 Examples: 110 >>> crypto = Crypto("BTCTRY") 111 >>> crypto.history(period="1mo") # Last month 112 >>> crypto.history(period="1y", interval="1wk") # Weekly for 1 year 113 >>> crypto.history(start="2024-01-01", end="2024-06-30") # Date range 114 """ 115 start_dt = self._parse_date(start) if start else None 116 end_dt = self._parse_date(end) if end else None 117 118 return self._provider.get_history( 119 pair=self._pair, 120 period=period, 121 interval=interval, 122 start=start_dt, 123 end=end_dt, 124 )
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
16class Fund(TechnicalMixin, TwitterMixin): 17 """ 18 A yfinance-like interface for mutual fund data from TEFAS. 19 20 Examples: 21 >>> import borsapy as bp 22 >>> fund = bp.Fund("AAK") 23 >>> fund.info 24 {'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...} 25 >>> fund.history(period="1mo") 26 Price FundSize Investors 27 Date 28 2024-12-01 1.200 150000000.0 5000 29 ... 30 31 >>> fund = bp.Fund("TTE") 32 >>> fund.info['return_1y'] 33 45.67 34 """ 35 36 def __init__(self, fund_code: str, fund_type: str | None = None): 37 """ 38 Initialize a Fund object. 39 40 Args: 41 fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") 42 fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. 43 If None, auto-detects by trying YAT first, then EMK. 44 45 Examples: 46 >>> fund = bp.Fund("AAK") # Investment fund (auto-detect) 47 >>> fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit) 48 """ 49 self._fund_code = fund_code.upper() 50 self._fund_type = fund_type.upper() if fund_type else None 51 self._provider = get_tefas_provider() 52 self._info_cache: dict[str, Any] | None = None 53 self._detected_fund_type: str | None = None 54 55 def _get_tweet_query(self) -> str: 56 name = None 57 try: 58 name = self.info.get("name") 59 except Exception: 60 pass 61 return _build_fund_query(self._fund_code, name) 62 63 @property 64 def fund_code(self) -> str: 65 """Return the fund code.""" 66 return self._fund_code 67 68 @property 69 def symbol(self) -> str: 70 """Return the fund code (alias).""" 71 return self._fund_code 72 73 @property 74 def fund_type(self) -> str: 75 """ 76 Return the fund type ("YAT" or "EMK"). 77 78 If not explicitly set, auto-detects on first history() or allocation() call. 79 """ 80 if self._fund_type: 81 return self._fund_type 82 if self._detected_fund_type: 83 return self._detected_fund_type 84 85 # Auto-detect by trying history with YAT first, then EMK 86 self._detect_fund_type() 87 return self._detected_fund_type or "YAT" 88 89 def _detect_fund_type(self) -> None: 90 """Auto-detect fund type by trying history API with different fund types.""" 91 if self._fund_type or self._detected_fund_type: 92 return 93 94 from datetime import timedelta 95 96 end_dt = datetime.now() 97 start_dt = end_dt - timedelta(days=7) 98 99 # Try YAT first 100 try: 101 df = self._provider._fetch_history_chunk( 102 self._fund_code, start_dt, end_dt, fund_type="YAT" 103 ) 104 if not df.empty: 105 self._detected_fund_type = "YAT" 106 return 107 except DataNotAvailableError: 108 pass 109 110 # Try EMK 111 try: 112 df = self._provider._fetch_history_chunk( 113 self._fund_code, start_dt, end_dt, fund_type="EMK" 114 ) 115 if not df.empty: 116 self._detected_fund_type = "EMK" 117 return 118 except DataNotAvailableError: 119 pass 120 121 # Default to YAT if neither works 122 self._detected_fund_type = "YAT" 123 124 @property 125 def info(self) -> dict[str, Any]: 126 """ 127 Get detailed fund information. 128 129 Returns: 130 Dictionary with fund details: 131 - fund_code: TEFAS fund code 132 - name: Fund full name 133 - date: Last update date 134 - price: Current unit price 135 - fund_size: Total fund size (TRY) 136 - investor_count: Number of investors 137 - founder: Fund founder company 138 - manager: Fund manager company 139 - fund_type: Fund type 140 - category: Fund category 141 - risk_value: Risk rating (1-7) 142 - return_1m, return_3m, return_6m: Period returns 143 - return_ytd: Year-to-date return 144 - return_1y, return_3y, return_5y: Annual returns 145 - daily_return: Daily return 146 """ 147 if self._info_cache is None: 148 # GetAllFundAnalyzeData works for both YAT and EMK without fontip 149 self._info_cache = self._provider.get_fund_detail(self._fund_code) 150 151 # If fund_type not explicitly set, we need to detect it for history/allocation 152 if not self._fund_type and not self._detected_fund_type: 153 # Detection will happen on first history() call 154 pass 155 156 return self._info_cache 157 158 @property 159 def detail(self) -> dict[str, Any]: 160 """Alias for info property.""" 161 return self.info 162 163 @property 164 def performance(self) -> dict[str, Any]: 165 """ 166 Get fund performance metrics only. 167 168 Returns: 169 Dictionary with performance data: 170 - daily_return: Daily return 171 - return_1m, return_3m, return_6m: Period returns 172 - return_ytd: Year-to-date return 173 - return_1y, return_3y, return_5y: Annual returns 174 """ 175 info = self.info 176 return { 177 "daily_return": info.get("daily_return"), 178 "return_1m": info.get("return_1m"), 179 "return_3m": info.get("return_3m"), 180 "return_6m": info.get("return_6m"), 181 "return_ytd": info.get("return_ytd"), 182 "return_1y": info.get("return_1y"), 183 "return_3y": info.get("return_3y"), 184 "return_5y": info.get("return_5y"), 185 } 186 187 @property 188 def management_fee(self) -> dict[str, Any]: 189 """ 190 Get management fee information for this fund. 191 192 Returns: 193 Dictionary with keys: 194 - applied_fee: Applied annual management fee (%) 195 - prospectus_fee: Prospectus management fee (%) 196 - max_expense_ratio: Maximum total expense ratio (%) 197 - annual_return: Annual return (%) 198 199 Examples: 200 >>> fund = bp.Fund("AAK") 201 >>> fund.management_fee 202 {'applied_fee': 1.0, 'prospectus_fee': 2.2, 'max_expense_ratio': 3.65, 'annual_return': 45.5} 203 """ 204 empty = { 205 "applied_fee": None, 206 "prospectus_fee": None, 207 "max_expense_ratio": None, 208 "annual_return": None, 209 } 210 211 try: 212 fees_list = self._provider.get_management_fees(fund_type=self.fund_type) 213 except Exception: 214 return empty 215 216 for item in fees_list: 217 if item.get("fund_code") == self._fund_code: 218 return { 219 "applied_fee": item.get("applied_fee"), 220 "prospectus_fee": item.get("prospectus_fee"), 221 "max_expense_ratio": item.get("max_expense_ratio"), 222 "annual_return": item.get("annual_return"), 223 } 224 225 return empty 226 227 @property 228 def tax_category(self) -> str | None: 229 """ 230 Get the tax category for this fund based on its TEFAS category. 231 232 Returns: 233 Tax category identifier string (e.g., "degisken_karma_doviz", 234 "pay_senedi_yogun"), or None if the category cannot be determined. 235 236 Examples: 237 >>> fund = bp.Fund("AAK") 238 >>> fund.tax_category 239 'borclanma_para_maden' 240 """ 241 from borsapy.tax import classify_fund_tax_category 242 243 info = self.info 244 category = info.get("category", "") or "" 245 fund_name = info.get("name", "") or "" 246 return classify_fund_tax_category(category, fund_name) 247 248 def withholding_tax_rate( 249 self, 250 purchase_date: datetime | str | None = None, 251 holding_days: int | None = None, 252 ) -> float | None: 253 """ 254 Get the withholding tax (stopaj) rate for this fund. 255 256 Args: 257 purchase_date: Date of fund purchase. Accepts datetime, date, or 258 "YYYY-MM-DD" string. Defaults to today. 259 holding_days: Number of days held. Relevant for GSYF/GYF funds 260 where >730 days qualifies for 0% rate. 261 262 Returns: 263 Tax rate as a decimal (e.g., 0.15 for 15%), or None if the 264 fund category cannot be determined. 265 266 Examples: 267 >>> fund = bp.Fund("AAK") 268 >>> fund.withholding_tax_rate("2025-06-01") 269 0.15 270 >>> fund.withholding_tax_rate("2025-08-01") 271 0.175 272 """ 273 from datetime import date 274 275 from borsapy.tax import get_withholding_tax_rate 276 277 cat = self.tax_category 278 if cat is None: 279 return None 280 if purchase_date is None: 281 purchase_date = date.today() 282 elif isinstance(purchase_date, datetime): 283 purchase_date = purchase_date.date() 284 return get_withholding_tax_rate(cat, purchase_date, holding_days) 285 286 @property 287 def allocation(self) -> pd.DataFrame: 288 """ 289 Get current portfolio allocation (asset breakdown) for last 7 days. 290 291 For longer periods, use allocation_history() method. 292 293 Returns: 294 DataFrame with columns: Date, asset_type, asset_name, weight. 295 296 Examples: 297 >>> fund = Fund("AAK") 298 >>> fund.allocation 299 Date asset_type asset_name weight 300 0 2024-12-20 HS Hisse Senedi 45.32 301 1 2024-12-20 DB Devlet Bonusu 30.15 302 ... 303 """ 304 return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type) 305 306 def allocation_history( 307 self, 308 period: str = "1mo", 309 start: datetime | str | None = None, 310 end: datetime | str | None = None, 311 ) -> pd.DataFrame: 312 """ 313 Get historical portfolio allocation (asset breakdown). 314 315 Note: TEFAS API supports maximum ~100 days (3 months) of data. 316 317 Args: 318 period: How much data to fetch. Valid periods: 319 1d, 5d, 1mo, 3mo (max ~100 days). 320 Ignored if start is provided. 321 start: Start date (string or datetime). 322 end: End date (string or datetime). Defaults to today. 323 324 Returns: 325 DataFrame with columns: Date, asset_type, asset_name, weight. 326 327 Examples: 328 >>> fund = Fund("AAK") 329 >>> fund.allocation_history(period="1mo") # Last month 330 >>> fund.allocation_history(period="3mo") # Last 3 months (max) 331 >>> fund.allocation_history(start="2024-10-01", end="2024-12-31") 332 """ 333 start_dt = self._parse_date(start) if start else None 334 end_dt = self._parse_date(end) if end else None 335 336 # If no start date, calculate from period 337 if start_dt is None: 338 from datetime import timedelta 339 end_dt = end_dt or datetime.now() 340 days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30) 341 # Cap at 100 days (API limit) 342 days = min(days, 100) 343 start_dt = end_dt - timedelta(days=days) 344 345 return self._provider.get_allocation( 346 fund_code=self._fund_code, 347 start=start_dt, 348 end=end_dt, 349 fund_type=self.fund_type, 350 ) 351 352 def history( 353 self, 354 period: str = "1mo", 355 start: datetime | str | None = None, 356 end: datetime | str | None = None, 357 ) -> pd.DataFrame: 358 """ 359 Get historical price data. 360 361 Args: 362 period: How much data to fetch. Valid periods: 363 1d, 5d, 1mo, 3mo, 6mo, 1y. 364 Ignored if start is provided. 365 start: Start date (string or datetime). 366 end: End date (string or datetime). Defaults to now. 367 368 Returns: 369 DataFrame with columns: Price, FundSize, Investors. 370 Index is the Date. 371 372 Examples: 373 >>> fund = Fund("AAK") 374 >>> fund.history(period="1mo") # Last month 375 >>> fund.history(period="1y") # Last year 376 >>> fund.history(start="2024-01-01", end="2024-06-30") # Date range 377 """ 378 start_dt = self._parse_date(start) if start else None 379 end_dt = self._parse_date(end) if end else None 380 381 return self._provider.get_history( 382 fund_code=self._fund_code, 383 period=period, 384 start=start_dt, 385 end=end_dt, 386 fund_type=self.fund_type, 387 ) 388 389 def _parse_date(self, date: str | datetime) -> datetime: 390 """Parse a date string to datetime.""" 391 if isinstance(date, datetime): 392 return date 393 for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]: 394 try: 395 return datetime.strptime(date, fmt) 396 except ValueError: 397 continue 398 raise ValueError(f"Could not parse date: {date}") 399 400 def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float: 401 """ 402 Calculate the Sharpe ratio for the fund. 403 404 Sharpe Ratio = (Rp - Rf) / σp 405 Where: 406 - Rp = Annualized return of the fund 407 - Rf = Risk-free rate (default: 10Y government bond yield) 408 - σp = Annualized standard deviation of returns 409 410 Args: 411 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 412 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 413 If None, uses current 10Y bond yield from bp.risk_free_rate(). 414 415 Returns: 416 Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent). 417 418 Examples: 419 >>> fund = bp.Fund("YAY") 420 >>> fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 421 0.85 422 423 >>> fund.sharpe_ratio(period="3y") # 3-year Sharpe 424 1.23 425 426 >>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate 427 0.92 428 """ 429 metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate) 430 return metrics.get("sharpe_ratio", np.nan) 431 432 def risk_metrics( 433 self, 434 period: str = "1y", 435 risk_free_rate: float | None = None, 436 ) -> dict[str, Any]: 437 """ 438 Calculate comprehensive risk metrics for the fund. 439 440 Args: 441 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 442 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 443 If None, uses current 10Y bond yield. 444 445 Returns: 446 Dictionary with risk metrics: 447 - annualized_return: Annualized return (%) 448 - annualized_volatility: Annualized standard deviation (%) 449 - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp 450 - sortino_ratio: Downside risk-adjusted return 451 - max_drawdown: Maximum peak-to-trough decline (%) 452 - risk_free_rate: Risk-free rate used (%) 453 - trading_days: Number of trading days in the period 454 455 Examples: 456 >>> fund = bp.Fund("YAY") 457 >>> metrics = fund.risk_metrics() 458 >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") 459 >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%") 460 """ 461 # Get historical data 462 df = self.history(period=period) 463 464 if df.empty or len(df) < 20: 465 return { 466 "annualized_return": np.nan, 467 "annualized_volatility": np.nan, 468 "sharpe_ratio": np.nan, 469 "sortino_ratio": np.nan, 470 "max_drawdown": np.nan, 471 "risk_free_rate": np.nan, 472 "trading_days": 0, 473 } 474 475 # Calculate daily returns 476 prices = df["Price"] 477 daily_returns = prices.pct_change().dropna() 478 trading_days = len(daily_returns) 479 480 # Annualization factor (trading days per year) 481 annualization_factor = 252 482 483 # Annualized return 484 total_return = (prices.iloc[-1] / prices.iloc[0]) - 1 485 years = trading_days / annualization_factor 486 annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100 487 488 # Annualized volatility 489 daily_volatility = daily_returns.std() 490 annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100 491 492 # Get risk-free rate 493 if risk_free_rate is None: 494 try: 495 from borsapy.bond import risk_free_rate as get_rf_rate 496 rf = get_rf_rate() * 100 # Returns decimal like 0.28, convert to % 497 except Exception: 498 rf = 30.0 # Fallback: approximate Turkish 10Y yield 499 else: 500 rf = risk_free_rate * 100 # Convert decimal to percentage 501 502 # Sharpe Ratio 503 if annualized_volatility > 0: 504 sharpe = (annualized_return - rf) / annualized_volatility 505 else: 506 sharpe = np.nan 507 508 # Sortino Ratio (uses downside deviation) 509 negative_returns = daily_returns[daily_returns < 0] 510 if len(negative_returns) > 0: 511 downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100 512 if downside_deviation > 0: 513 sortino = (annualized_return - rf) / downside_deviation 514 else: 515 sortino = np.nan 516 else: 517 sortino = np.inf # No negative returns 518 519 # Maximum Drawdown 520 cumulative = (1 + daily_returns).cumprod() 521 running_max = cumulative.cummax() 522 drawdowns = (cumulative - running_max) / running_max 523 max_drawdown = drawdowns.min() * 100 # Negative percentage 524 525 return { 526 "annualized_return": round(annualized_return, 2), 527 "annualized_volatility": round(annualized_volatility, 2), 528 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 529 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 530 "max_drawdown": round(max_drawdown, 2), 531 "risk_free_rate": round(rf, 2), 532 "trading_days": trading_days, 533 } 534 535 def get_holdings( 536 self, 537 api_key: str, 538 period: str | None = None, 539 ) -> pd.DataFrame: 540 """ 541 Get detailed portfolio holdings (individual securities). 542 543 Returns the specific stocks, ETFs, and funds held by this fund, 544 with their weights and ISIN codes. Data is sourced from KAP 545 "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures. 546 547 Uses OpenRouter LLM for PDF parsing. 548 549 Args: 550 api_key: OpenRouter API key for LLM parsing. 551 Get your free API key at: https://openrouter.ai/ 552 period: Optional period in format "YYYY-MM" (e.g., "2025-12"). 553 If None, returns the most recent holdings. 554 555 Returns: 556 DataFrame with columns: 557 - symbol: Security symbol (e.g., "GOOGL", "THYAO") 558 - isin: ISIN code 559 - name: Full security name 560 - weight: Portfolio weight (%) 561 - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) 562 - country: Country ('TR', 'US', or None) 563 - value: Market value in TRY 564 565 Raises: 566 DataNotAvailableError: If holdings data not available. 567 APIError: If LLM parsing fails. 568 ImportError: If required packages are not installed. 569 570 Examples: 571 >>> fund = bp.Fund("YAY") 572 >>> fund.get_holdings(api_key="sk-or-v1-...") 573 symbol isin name weight type country value 574 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 575 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 576 ... 577 578 >>> # Get holdings for specific period 579 >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12") 580 581 >>> # Filter by type 582 >>> holdings = fund.get_holdings(api_key="sk-or-v1-...") 583 >>> holdings[holdings['type'] == 'stock'] 584 """ 585 from borsapy._providers.kap_holdings import get_kap_holdings_provider 586 587 provider = get_kap_holdings_provider() 588 return provider.get_holdings_df(self._fund_code, api_key, period=period) 589 590 def __repr__(self) -> str: 591 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
36 def __init__(self, fund_code: str, fund_type: str | None = None): 37 """ 38 Initialize a Fund object. 39 40 Args: 41 fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") 42 fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. 43 If None, auto-detects by trying YAT first, then EMK. 44 45 Examples: 46 >>> fund = bp.Fund("AAK") # Investment fund (auto-detect) 47 >>> fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit) 48 """ 49 self._fund_code = fund_code.upper() 50 self._fund_type = fund_type.upper() if fund_type else None 51 self._provider = get_tefas_provider() 52 self._info_cache: dict[str, Any] | None = None 53 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)
63 @property 64 def fund_code(self) -> str: 65 """Return the fund code.""" 66 return self._fund_code
Return the fund code.
68 @property 69 def symbol(self) -> str: 70 """Return the fund code (alias).""" 71 return self._fund_code
Return the fund code (alias).
73 @property 74 def fund_type(self) -> str: 75 """ 76 Return the fund type ("YAT" or "EMK"). 77 78 If not explicitly set, auto-detects on first history() or allocation() call. 79 """ 80 if self._fund_type: 81 return self._fund_type 82 if self._detected_fund_type: 83 return self._detected_fund_type 84 85 # Auto-detect by trying history with YAT first, then EMK 86 self._detect_fund_type() 87 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.
124 @property 125 def info(self) -> dict[str, Any]: 126 """ 127 Get detailed fund information. 128 129 Returns: 130 Dictionary with fund details: 131 - fund_code: TEFAS fund code 132 - name: Fund full name 133 - date: Last update date 134 - price: Current unit price 135 - fund_size: Total fund size (TRY) 136 - investor_count: Number of investors 137 - founder: Fund founder company 138 - manager: Fund manager company 139 - fund_type: Fund type 140 - category: Fund category 141 - risk_value: Risk rating (1-7) 142 - return_1m, return_3m, return_6m: Period returns 143 - return_ytd: Year-to-date return 144 - return_1y, return_3y, return_5y: Annual returns 145 - daily_return: Daily return 146 """ 147 if self._info_cache is None: 148 # GetAllFundAnalyzeData works for both YAT and EMK without fontip 149 self._info_cache = self._provider.get_fund_detail(self._fund_code) 150 151 # If fund_type not explicitly set, we need to detect it for history/allocation 152 if not self._fund_type and not self._detected_fund_type: 153 # Detection will happen on first history() call 154 pass 155 156 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
158 @property 159 def detail(self) -> dict[str, Any]: 160 """Alias for info property.""" 161 return self.info
Alias for info property.
163 @property 164 def performance(self) -> dict[str, Any]: 165 """ 166 Get fund performance metrics only. 167 168 Returns: 169 Dictionary with performance data: 170 - daily_return: Daily return 171 - return_1m, return_3m, return_6m: Period returns 172 - return_ytd: Year-to-date return 173 - return_1y, return_3y, return_5y: Annual returns 174 """ 175 info = self.info 176 return { 177 "daily_return": info.get("daily_return"), 178 "return_1m": info.get("return_1m"), 179 "return_3m": info.get("return_3m"), 180 "return_6m": info.get("return_6m"), 181 "return_ytd": info.get("return_ytd"), 182 "return_1y": info.get("return_1y"), 183 "return_3y": info.get("return_3y"), 184 "return_5y": info.get("return_5y"), 185 }
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
187 @property 188 def management_fee(self) -> dict[str, Any]: 189 """ 190 Get management fee information for this fund. 191 192 Returns: 193 Dictionary with keys: 194 - applied_fee: Applied annual management fee (%) 195 - prospectus_fee: Prospectus management fee (%) 196 - max_expense_ratio: Maximum total expense ratio (%) 197 - annual_return: Annual return (%) 198 199 Examples: 200 >>> fund = bp.Fund("AAK") 201 >>> fund.management_fee 202 {'applied_fee': 1.0, 'prospectus_fee': 2.2, 'max_expense_ratio': 3.65, 'annual_return': 45.5} 203 """ 204 empty = { 205 "applied_fee": None, 206 "prospectus_fee": None, 207 "max_expense_ratio": None, 208 "annual_return": None, 209 } 210 211 try: 212 fees_list = self._provider.get_management_fees(fund_type=self.fund_type) 213 except Exception: 214 return empty 215 216 for item in fees_list: 217 if item.get("fund_code") == self._fund_code: 218 return { 219 "applied_fee": item.get("applied_fee"), 220 "prospectus_fee": item.get("prospectus_fee"), 221 "max_expense_ratio": item.get("max_expense_ratio"), 222 "annual_return": item.get("annual_return"), 223 } 224 225 return empty
Get management fee information for this fund.
Returns: Dictionary with keys: - applied_fee: Applied annual management fee (%) - prospectus_fee: Prospectus management fee (%) - max_expense_ratio: Maximum total expense ratio (%) - annual_return: Annual return (%)
Examples:
fund = bp.Fund("AAK") fund.management_fee {'applied_fee': 1.0, 'prospectus_fee': 2.2, 'max_expense_ratio': 3.65, 'annual_return': 45.5}
227 @property 228 def tax_category(self) -> str | None: 229 """ 230 Get the tax category for this fund based on its TEFAS category. 231 232 Returns: 233 Tax category identifier string (e.g., "degisken_karma_doviz", 234 "pay_senedi_yogun"), or None if the category cannot be determined. 235 236 Examples: 237 >>> fund = bp.Fund("AAK") 238 >>> fund.tax_category 239 'borclanma_para_maden' 240 """ 241 from borsapy.tax import classify_fund_tax_category 242 243 info = self.info 244 category = info.get("category", "") or "" 245 fund_name = info.get("name", "") or "" 246 return classify_fund_tax_category(category, fund_name)
Get the tax category for this fund based on its TEFAS category.
Returns: Tax category identifier string (e.g., "degisken_karma_doviz", "pay_senedi_yogun"), or None if the category cannot be determined.
Examples:
fund = bp.Fund("AAK") fund.tax_category 'borclanma_para_maden'
248 def withholding_tax_rate( 249 self, 250 purchase_date: datetime | str | None = None, 251 holding_days: int | None = None, 252 ) -> float | None: 253 """ 254 Get the withholding tax (stopaj) rate for this fund. 255 256 Args: 257 purchase_date: Date of fund purchase. Accepts datetime, date, or 258 "YYYY-MM-DD" string. Defaults to today. 259 holding_days: Number of days held. Relevant for GSYF/GYF funds 260 where >730 days qualifies for 0% rate. 261 262 Returns: 263 Tax rate as a decimal (e.g., 0.15 for 15%), or None if the 264 fund category cannot be determined. 265 266 Examples: 267 >>> fund = bp.Fund("AAK") 268 >>> fund.withholding_tax_rate("2025-06-01") 269 0.15 270 >>> fund.withholding_tax_rate("2025-08-01") 271 0.175 272 """ 273 from datetime import date 274 275 from borsapy.tax import get_withholding_tax_rate 276 277 cat = self.tax_category 278 if cat is None: 279 return None 280 if purchase_date is None: 281 purchase_date = date.today() 282 elif isinstance(purchase_date, datetime): 283 purchase_date = purchase_date.date() 284 return get_withholding_tax_rate(cat, purchase_date, holding_days)
Get the withholding tax (stopaj) rate for this fund.
Args: purchase_date: Date of fund purchase. Accepts datetime, date, or "YYYY-MM-DD" string. Defaults to today. holding_days: Number of days held. Relevant for GSYF/GYF funds where >730 days qualifies for 0% rate.
Returns: Tax rate as a decimal (e.g., 0.15 for 15%), or None if the fund category cannot be determined.
Examples:
fund = bp.Fund("AAK") fund.withholding_tax_rate("2025-06-01") 0.15 fund.withholding_tax_rate("2025-08-01") 0.175
286 @property 287 def allocation(self) -> pd.DataFrame: 288 """ 289 Get current portfolio allocation (asset breakdown) for last 7 days. 290 291 For longer periods, use allocation_history() method. 292 293 Returns: 294 DataFrame with columns: Date, asset_type, asset_name, weight. 295 296 Examples: 297 >>> fund = Fund("AAK") 298 >>> fund.allocation 299 Date asset_type asset_name weight 300 0 2024-12-20 HS Hisse Senedi 45.32 301 1 2024-12-20 DB Devlet Bonusu 30.15 302 ... 303 """ 304 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 ...
306 def allocation_history( 307 self, 308 period: str = "1mo", 309 start: datetime | str | None = None, 310 end: datetime | str | None = None, 311 ) -> pd.DataFrame: 312 """ 313 Get historical portfolio allocation (asset breakdown). 314 315 Note: TEFAS API supports maximum ~100 days (3 months) of data. 316 317 Args: 318 period: How much data to fetch. Valid periods: 319 1d, 5d, 1mo, 3mo (max ~100 days). 320 Ignored if start is provided. 321 start: Start date (string or datetime). 322 end: End date (string or datetime). Defaults to today. 323 324 Returns: 325 DataFrame with columns: Date, asset_type, asset_name, weight. 326 327 Examples: 328 >>> fund = Fund("AAK") 329 >>> fund.allocation_history(period="1mo") # Last month 330 >>> fund.allocation_history(period="3mo") # Last 3 months (max) 331 >>> fund.allocation_history(start="2024-10-01", end="2024-12-31") 332 """ 333 start_dt = self._parse_date(start) if start else None 334 end_dt = self._parse_date(end) if end else None 335 336 # If no start date, calculate from period 337 if start_dt is None: 338 from datetime import timedelta 339 end_dt = end_dt or datetime.now() 340 days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30) 341 # Cap at 100 days (API limit) 342 days = min(days, 100) 343 start_dt = end_dt - timedelta(days=days) 344 345 return self._provider.get_allocation( 346 fund_code=self._fund_code, 347 start=start_dt, 348 end=end_dt, 349 fund_type=self.fund_type, 350 )
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")
352 def history( 353 self, 354 period: str = "1mo", 355 start: datetime | str | None = None, 356 end: datetime | str | None = None, 357 ) -> pd.DataFrame: 358 """ 359 Get historical price data. 360 361 Args: 362 period: How much data to fetch. Valid periods: 363 1d, 5d, 1mo, 3mo, 6mo, 1y. 364 Ignored if start is provided. 365 start: Start date (string or datetime). 366 end: End date (string or datetime). Defaults to now. 367 368 Returns: 369 DataFrame with columns: Price, FundSize, Investors. 370 Index is the Date. 371 372 Examples: 373 >>> fund = Fund("AAK") 374 >>> fund.history(period="1mo") # Last month 375 >>> fund.history(period="1y") # Last year 376 >>> fund.history(start="2024-01-01", end="2024-06-30") # Date range 377 """ 378 start_dt = self._parse_date(start) if start else None 379 end_dt = self._parse_date(end) if end else None 380 381 return self._provider.get_history( 382 fund_code=self._fund_code, 383 period=period, 384 start=start_dt, 385 end=end_dt, 386 fund_type=self.fund_type, 387 )
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
400 def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float: 401 """ 402 Calculate the Sharpe ratio for the fund. 403 404 Sharpe Ratio = (Rp - Rf) / σp 405 Where: 406 - Rp = Annualized return of the fund 407 - Rf = Risk-free rate (default: 10Y government bond yield) 408 - σp = Annualized standard deviation of returns 409 410 Args: 411 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 412 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 413 If None, uses current 10Y bond yield from bp.risk_free_rate(). 414 415 Returns: 416 Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent). 417 418 Examples: 419 >>> fund = bp.Fund("YAY") 420 >>> fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 421 0.85 422 423 >>> fund.sharpe_ratio(period="3y") # 3-year Sharpe 424 1.23 425 426 >>> fund.sharpe_ratio(risk_free_rate=0.25) # Custom risk-free rate 427 0.92 428 """ 429 metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate) 430 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
432 def risk_metrics( 433 self, 434 period: str = "1y", 435 risk_free_rate: float | None = None, 436 ) -> dict[str, Any]: 437 """ 438 Calculate comprehensive risk metrics for the fund. 439 440 Args: 441 period: Period for calculation ("1y", "3y", "5y"). Default is "1y". 442 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 443 If None, uses current 10Y bond yield. 444 445 Returns: 446 Dictionary with risk metrics: 447 - annualized_return: Annualized return (%) 448 - annualized_volatility: Annualized standard deviation (%) 449 - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp 450 - sortino_ratio: Downside risk-adjusted return 451 - max_drawdown: Maximum peak-to-trough decline (%) 452 - risk_free_rate: Risk-free rate used (%) 453 - trading_days: Number of trading days in the period 454 455 Examples: 456 >>> fund = bp.Fund("YAY") 457 >>> metrics = fund.risk_metrics() 458 >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") 459 >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%") 460 """ 461 # Get historical data 462 df = self.history(period=period) 463 464 if df.empty or len(df) < 20: 465 return { 466 "annualized_return": np.nan, 467 "annualized_volatility": np.nan, 468 "sharpe_ratio": np.nan, 469 "sortino_ratio": np.nan, 470 "max_drawdown": np.nan, 471 "risk_free_rate": np.nan, 472 "trading_days": 0, 473 } 474 475 # Calculate daily returns 476 prices = df["Price"] 477 daily_returns = prices.pct_change().dropna() 478 trading_days = len(daily_returns) 479 480 # Annualization factor (trading days per year) 481 annualization_factor = 252 482 483 # Annualized return 484 total_return = (prices.iloc[-1] / prices.iloc[0]) - 1 485 years = trading_days / annualization_factor 486 annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100 487 488 # Annualized volatility 489 daily_volatility = daily_returns.std() 490 annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100 491 492 # Get risk-free rate 493 if risk_free_rate is None: 494 try: 495 from borsapy.bond import risk_free_rate as get_rf_rate 496 rf = get_rf_rate() * 100 # Returns decimal like 0.28, convert to % 497 except Exception: 498 rf = 30.0 # Fallback: approximate Turkish 10Y yield 499 else: 500 rf = risk_free_rate * 100 # Convert decimal to percentage 501 502 # Sharpe Ratio 503 if annualized_volatility > 0: 504 sharpe = (annualized_return - rf) / annualized_volatility 505 else: 506 sharpe = np.nan 507 508 # Sortino Ratio (uses downside deviation) 509 negative_returns = daily_returns[daily_returns < 0] 510 if len(negative_returns) > 0: 511 downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100 512 if downside_deviation > 0: 513 sortino = (annualized_return - rf) / downside_deviation 514 else: 515 sortino = np.nan 516 else: 517 sortino = np.inf # No negative returns 518 519 # Maximum Drawdown 520 cumulative = (1 + daily_returns).cumprod() 521 running_max = cumulative.cummax() 522 drawdowns = (cumulative - running_max) / running_max 523 max_drawdown = drawdowns.min() * 100 # Negative percentage 524 525 return { 526 "annualized_return": round(annualized_return, 2), 527 "annualized_volatility": round(annualized_volatility, 2), 528 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 529 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 530 "max_drawdown": round(max_drawdown, 2), 531 "risk_free_rate": round(rf, 2), 532 "trading_days": trading_days, 533 }
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}%")
535 def get_holdings( 536 self, 537 api_key: str, 538 period: str | None = None, 539 ) -> pd.DataFrame: 540 """ 541 Get detailed portfolio holdings (individual securities). 542 543 Returns the specific stocks, ETFs, and funds held by this fund, 544 with their weights and ISIN codes. Data is sourced from KAP 545 "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures. 546 547 Uses OpenRouter LLM for PDF parsing. 548 549 Args: 550 api_key: OpenRouter API key for LLM parsing. 551 Get your free API key at: https://openrouter.ai/ 552 period: Optional period in format "YYYY-MM" (e.g., "2025-12"). 553 If None, returns the most recent holdings. 554 555 Returns: 556 DataFrame with columns: 557 - symbol: Security symbol (e.g., "GOOGL", "THYAO") 558 - isin: ISIN code 559 - name: Full security name 560 - weight: Portfolio weight (%) 561 - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) 562 - country: Country ('TR', 'US', or None) 563 - value: Market value in TRY 564 565 Raises: 566 DataNotAvailableError: If holdings data not available. 567 APIError: If LLM parsing fails. 568 ImportError: If required packages are not installed. 569 570 Examples: 571 >>> fund = bp.Fund("YAY") 572 >>> fund.get_holdings(api_key="sk-or-v1-...") 573 symbol isin name weight type country value 574 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 575 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 576 ... 577 578 >>> # Get holdings for specific period 579 >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12") 580 581 >>> # Filter by type 582 >>> holdings = fund.get_holdings(api_key="sk-or-v1-...") 583 >>> holdings[holdings['type'] == 'stock'] 584 """ 585 from borsapy._providers.kap_holdings import get_kap_holdings_provider 586 587 provider = get_kap_holdings_provider() 588 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 self._target_weights: dict[str, float] = {} 154 155 # === Asset Management === 156 157 def add( 158 self, 159 symbol: str, 160 shares: float, 161 cost: float | None = None, 162 asset_type: str | None = None, 163 purchase_date: str | date | datetime | None = None, 164 ) -> "Portfolio": 165 """ 166 Add an asset to the portfolio. 167 168 Args: 169 symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) 170 shares: Number of shares/units. 171 cost: Cost per share/unit. If None, uses current price. 172 asset_type: Asset type override. Auto-detected if None. 173 Valid values: "stock", "fx", "crypto", "fund" 174 purchase_date: Date when the asset was purchased. 175 Accepts string (YYYY-MM-DD), date, or datetime. 176 If None, defaults to today. 177 178 Returns: 179 Self for method chaining. 180 181 Examples: 182 >>> p = Portfolio() 183 >>> p.add("THYAO", shares=100, cost=280) # Stock with cost 184 >>> p.add("GARAN", shares=200) # Stock at current price 185 >>> p.add("gram-altin", shares=5, asset_type="fx") # Metal 186 >>> p.add("YAY", shares=500, asset_type="fund") # Mutual fund 187 >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15") 188 """ 189 symbol = symbol.upper() if asset_type != "fx" else symbol 190 191 # Detect or validate asset type 192 if asset_type is None: 193 detected_type = _detect_asset_type(symbol) 194 else: 195 detected_type = asset_type # type: ignore 196 197 # Get current price if cost not provided 198 if cost is None: 199 asset = self._get_or_create_asset(symbol, detected_type) 200 cost = self._get_current_price(asset) 201 202 # Parse purchase_date 203 parsed_date: date | None = None 204 if purchase_date is not None: 205 if isinstance(purchase_date, str): 206 parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date() 207 elif isinstance(purchase_date, datetime): 208 parsed_date = purchase_date.date() 209 elif isinstance(purchase_date, date): 210 parsed_date = purchase_date 211 else: 212 parsed_date = date.today() 213 214 self._holdings[symbol] = Holding( 215 symbol=symbol, 216 shares=shares, 217 cost_per_share=cost, 218 asset_type=detected_type, 219 purchase_date=parsed_date, 220 ) 221 222 return self 223 224 def remove(self, symbol: str) -> "Portfolio": 225 """ 226 Remove an asset from the portfolio. 227 228 Args: 229 symbol: Asset symbol to remove. 230 231 Returns: 232 Self for method chaining. 233 """ 234 symbol_upper = symbol.upper() 235 236 # Try both original and uppercase 237 if symbol in self._holdings: 238 del self._holdings[symbol] 239 self._asset_cache.pop(symbol, None) 240 elif symbol_upper in self._holdings: 241 del self._holdings[symbol_upper] 242 self._asset_cache.pop(symbol_upper, None) 243 244 return self 245 246 def update( 247 self, 248 symbol: str, 249 shares: float | None = None, 250 cost: float | None = None, 251 ) -> "Portfolio": 252 """ 253 Update an existing holding. 254 255 Args: 256 symbol: Asset symbol. 257 shares: New share count. If None, keeps existing. 258 cost: New cost per share. If None, keeps existing. 259 260 Returns: 261 Self for method chaining. 262 """ 263 if symbol not in self._holdings: 264 symbol = symbol.upper() 265 if symbol not in self._holdings: 266 raise KeyError(f"Symbol {symbol} not in portfolio") 267 268 holding = self._holdings[symbol] 269 if shares is not None: 270 holding.shares = shares 271 if cost is not None: 272 holding.cost_per_share = cost 273 274 return self 275 276 def clear(self) -> "Portfolio": 277 """ 278 Remove all holdings from the portfolio. 279 280 Returns: 281 Self for method chaining. 282 """ 283 self._holdings.clear() 284 self._asset_cache.clear() 285 return self 286 287 def set_benchmark(self, index: str) -> "Portfolio": 288 """ 289 Set the benchmark index for beta/alpha calculations. 290 291 Args: 292 index: Index symbol (XU100, XU030, XK030, etc.) 293 294 Returns: 295 Self for method chaining. 296 """ 297 self._benchmark = index 298 return self 299 300 # === Rebalancing === 301 302 def set_target_weights(self, weights: dict[str, float]) -> "Portfolio": 303 """Set target allocation weights for rebalancing. 304 305 Args: 306 weights: Dict of symbol -> target weight (0.0 to 1.0 scale). 307 Must sum to approximately 1.0 (tolerance: 0.01). 308 309 Returns: 310 Self for method chaining. 311 312 Raises: 313 ValueError: If weights don't sum to ~1.0 or contain invalid values. 314 315 Examples: 316 >>> p = Portfolio() 317 >>> p.add("THYAO", shares=100, cost=280) 318 >>> p.add("GARAN", shares=200, cost=50) 319 >>> p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40}) 320 """ 321 total = sum(weights.values()) 322 if abs(total - 1.0) > 0.01: 323 raise ValueError( 324 f"Target weights must sum to ~1.0, got {total:.4f}" 325 ) 326 for symbol, weight in weights.items(): 327 if weight < 0 or weight > 1: 328 raise ValueError( 329 f"Weight for {symbol} must be between 0.0 and 1.0, got {weight}" 330 ) 331 self._target_weights = dict(weights) 332 return self 333 334 @property 335 def target_weights(self) -> dict[str, float]: 336 """Get current target allocation weights.""" 337 return dict(self._target_weights) 338 339 def drift(self) -> pd.DataFrame: 340 """Calculate drift between current and target weights. 341 342 Returns: 343 DataFrame with columns: 344 - symbol: Asset symbol 345 - current_weight: Current portfolio weight (0-1 scale) 346 - target_weight: Target allocation weight (0-1 scale) 347 - drift: Absolute drift (current - target) 348 - drift_pct: Drift as percentage points 349 350 Raises: 351 ValueError: If target weights are not set. 352 """ 353 if not self._target_weights: 354 raise ValueError( 355 "Target weights not set. Call set_target_weights() first." 356 ) 357 358 current_weights = self.weights # dict[str, float] on 0-1 scale 359 rows = [] 360 361 # Include all symbols from both current holdings and targets 362 all_symbols = set(current_weights.keys()) | set(self._target_weights.keys()) 363 364 for symbol in sorted(all_symbols): 365 current = current_weights.get(symbol, 0.0) 366 target = self._target_weights.get(symbol, 0.0) 367 drift_val = current - target 368 rows.append({ 369 "symbol": symbol, 370 "current_weight": round(current, 4), 371 "target_weight": round(target, 4), 372 "drift": round(drift_val, 4), 373 "drift_pct": round(drift_val * 100, 2), 374 }) 375 376 return pd.DataFrame(rows) 377 378 def rebalance_plan(self, threshold: float = 0.0) -> pd.DataFrame: 379 """Calculate trades needed to rebalance portfolio. 380 381 Args: 382 threshold: Minimum drift (0-1 scale) to trigger a trade. 383 E.g., 0.02 = ignore drifts less than 2%. 384 385 Returns: 386 DataFrame with columns: 387 - symbol: Asset symbol 388 - current_shares: Current number of shares 389 - target_shares: Target number of shares 390 - delta_shares: Shares to buy (+) or sell (-) 391 - delta_value: Approximate trade value in TL 392 - action: "BUY", "SELL", or "HOLD" 393 394 Raises: 395 ValueError: If target weights are not set. 396 """ 397 if not self._target_weights: 398 raise ValueError( 399 "Target weights not set. Call set_target_weights() first." 400 ) 401 402 total_value = self.value 403 if total_value == 0: 404 return pd.DataFrame( 405 columns=["symbol", "current_shares", "target_shares", 406 "delta_shares", "delta_value", "action"] 407 ) 408 409 rows = [] 410 all_symbols = set(self._holdings.keys()) | set(self._target_weights.keys()) 411 412 for symbol in sorted(all_symbols): 413 holding = self._holdings.get(symbol) 414 target_weight = self._target_weights.get(symbol, 0.0) 415 416 # Get current price 417 if holding: 418 asset = self._get_or_create_asset(symbol, holding.asset_type) 419 else: 420 # Symbol in targets but not in holdings - detect type 421 detected_type = _detect_asset_type(symbol) 422 asset = self._get_or_create_asset(symbol, detected_type) 423 current_price = self._get_current_price(asset) 424 425 current_shares = holding.shares if holding else 0.0 426 current_value = current_shares * current_price 427 current_weight = current_value / total_value if total_value else 0.0 428 429 # Check threshold 430 if abs(current_weight - target_weight) < threshold: 431 rows.append({ 432 "symbol": symbol, 433 "current_shares": current_shares, 434 "target_shares": current_shares, 435 "delta_shares": 0.0, 436 "delta_value": 0.0, 437 "action": "HOLD", 438 }) 439 continue 440 441 # Calculate target shares 442 target_value = total_value * target_weight 443 if current_price > 0: 444 target_shares = target_value / current_price 445 else: 446 target_shares = 0.0 447 448 # Round stock shares to integers 449 asset_type = holding.asset_type if holding else _detect_asset_type(symbol) 450 if asset_type == "stock": 451 target_shares = round(target_shares) 452 453 delta_shares = target_shares - current_shares 454 delta_value = delta_shares * current_price 455 456 if delta_shares > 0.001: 457 action = "BUY" 458 elif delta_shares < -0.001: 459 action = "SELL" 460 else: 461 action = "HOLD" 462 463 rows.append({ 464 "symbol": symbol, 465 "current_shares": current_shares, 466 "target_shares": round(target_shares, 4), 467 "delta_shares": round(delta_shares, 4), 468 "delta_value": round(delta_value, 2), 469 "action": action, 470 }) 471 472 return pd.DataFrame(rows) 473 474 def rebalance(self, threshold: float = 0.0, dry_run: bool = False) -> pd.DataFrame: 475 """Execute rebalance by updating share counts. 476 477 Updates the portfolio holdings to match target weights. 478 Stock shares are rounded to integers; crypto and fund shares 479 remain fractional. 480 481 Args: 482 threshold: Minimum drift (0-1 scale) to trigger a trade. 483 dry_run: If True, return the plan without executing. 484 485 Returns: 486 DataFrame with the rebalance plan (same as rebalance_plan()). 487 488 Raises: 489 ValueError: If target weights are not set. 490 """ 491 plan = self.rebalance_plan(threshold=threshold) 492 493 if dry_run or plan.empty: 494 return plan 495 496 for _, row in plan.iterrows(): 497 symbol = row["symbol"] 498 action = row["action"] 499 target_shares = row["target_shares"] 500 501 if action == "HOLD": 502 continue 503 504 if symbol in self._holdings: 505 if target_shares <= 0: 506 self.remove(symbol) 507 else: 508 self._holdings[symbol].shares = target_shares 509 elif target_shares > 0: 510 # New holding needed 511 detected_type = _detect_asset_type(symbol) 512 asset = self._get_or_create_asset(symbol, detected_type) 513 price = self._get_current_price(asset) 514 self._holdings[symbol] = Holding( 515 symbol=symbol, 516 shares=target_shares, 517 cost_per_share=price, 518 asset_type=detected_type, 519 purchase_date=date.today(), 520 ) 521 522 return plan 523 524 # === Properties === 525 526 @property 527 def holdings(self) -> pd.DataFrame: 528 """ 529 Get all holdings as a DataFrame. 530 531 Returns: 532 DataFrame with columns: 533 - symbol: Asset symbol 534 - shares: Number of shares 535 - cost: Cost per share 536 - current_price: Current price 537 - value: Current value (shares * price) 538 - weight: Portfolio weight (%) 539 - pnl: Profit/loss (TL) 540 - pnl_pct: Profit/loss (%) 541 - asset_type: Asset type 542 - purchase_date: Date when asset was purchased 543 - holding_days: Number of days since purchase 544 """ 545 if not self._holdings: 546 return pd.DataFrame( 547 columns=[ 548 "symbol", "shares", "cost", "current_price", 549 "value", "weight", "pnl", "pnl_pct", "asset_type", 550 "purchase_date", "holding_days" 551 ] 552 ) 553 554 rows = [] 555 total_value = self.value 556 today = date.today() 557 558 for symbol, holding in self._holdings.items(): 559 asset = self._get_or_create_asset(symbol, holding.asset_type) 560 current_price = self._get_current_price(asset) 561 value = holding.shares * current_price 562 cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0 563 pnl = value - cost_basis if cost_basis else 0 564 pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0 565 weight = (value / total_value * 100) if total_value else 0 566 567 # Calculate holding days 568 holding_days = None 569 if holding.purchase_date: 570 holding_days = (today - holding.purchase_date).days 571 572 rows.append({ 573 "symbol": symbol, 574 "shares": holding.shares, 575 "cost": holding.cost_per_share, 576 "current_price": current_price, 577 "value": value, 578 "weight": round(weight, 2), 579 "pnl": round(pnl, 2), 580 "pnl_pct": round(pnl_pct, 2), 581 "asset_type": holding.asset_type, 582 "purchase_date": holding.purchase_date, 583 "holding_days": holding_days, 584 }) 585 586 return pd.DataFrame(rows) 587 588 @property 589 def symbols(self) -> list[str]: 590 """Get list of symbols in portfolio.""" 591 return list(self._holdings.keys()) 592 593 @property 594 def value(self) -> float: 595 """Get total portfolio value in TL.""" 596 total = 0.0 597 for symbol, holding in self._holdings.items(): 598 asset = self._get_or_create_asset(symbol, holding.asset_type) 599 price = self._get_current_price(asset) 600 total += holding.shares * price 601 return total 602 603 @property 604 def cost(self) -> float: 605 """Get total portfolio cost basis in TL.""" 606 total = 0.0 607 for holding in self._holdings.values(): 608 if holding.cost_per_share: 609 total += holding.shares * holding.cost_per_share 610 return total 611 612 @property 613 def pnl(self) -> float: 614 """Get total profit/loss in TL.""" 615 return self.value - self.cost 616 617 @property 618 def pnl_pct(self) -> float: 619 """Get total profit/loss as percentage.""" 620 cost = self.cost 621 if cost == 0: 622 return 0.0 623 return (self.pnl / cost) * 100 624 625 @property 626 def weights(self) -> dict[str, float]: 627 """Get portfolio weights as dictionary.""" 628 total_value = self.value 629 if total_value == 0: 630 return {} 631 632 result = {} 633 for symbol, holding in self._holdings.items(): 634 asset = self._get_or_create_asset(symbol, holding.asset_type) 635 price = self._get_current_price(asset) 636 value = holding.shares * price 637 result[symbol] = round(value / total_value, 4) 638 return result 639 640 # === Performance === 641 642 def history(self, period: str = "1y") -> pd.DataFrame: 643 """ 644 Get historical portfolio value based on current holdings. 645 646 Note: Uses current share counts - does not track historical trades. 647 When purchase_date is set for a holding, only data from that date 648 onwards is included in the portfolio value calculation. 649 650 Args: 651 period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y). 652 653 Returns: 654 DataFrame with columns: Value, Daily_Return. 655 Index is Date. 656 """ 657 if not self._holdings: 658 return pd.DataFrame(columns=["Value", "Daily_Return"]) 659 660 all_prices = {} 661 for symbol, holding in self._holdings.items(): 662 asset = self._get_or_create_asset(symbol, holding.asset_type) 663 try: 664 hist = asset.history(period=period) 665 if hist.empty: 666 continue 667 668 # Filter by purchase_date if set 669 if holding.purchase_date: 670 # Handle both timezone-aware and timezone-naive indices 671 if hasattr(hist.index, 'tz') and hist.index.tz is not None: 672 hist = hist[hist.index.date >= holding.purchase_date] 673 else: 674 hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)] 675 676 if hist.empty: 677 continue 678 679 # Use Close for stocks/index, Price for funds 680 price_col = "Close" if "Close" in hist.columns else "Price" 681 all_prices[symbol] = hist[price_col] * holding.shares 682 except Exception: 683 continue 684 685 if not all_prices: 686 return pd.DataFrame(columns=["Value", "Daily_Return"]) 687 688 df = pd.DataFrame(all_prices) 689 df = df.dropna(how="all") 690 df["Value"] = df.sum(axis=1) 691 df["Daily_Return"] = df["Value"].pct_change() 692 return df[["Value", "Daily_Return"]] 693 694 @property 695 def performance(self) -> dict[str, float]: 696 """ 697 Get portfolio performance summary. 698 699 Returns: 700 Dictionary with: 701 - total_return: Total return (%) 702 - annualized_return: Annualized return (%) 703 - total_value: Current value (TL) 704 - total_cost: Total cost (TL) 705 - total_pnl: Profit/loss (TL) 706 """ 707 return { 708 "total_return": self.pnl_pct, 709 "annualized_return": np.nan, # Calculated in risk_metrics 710 "total_value": self.value, 711 "total_cost": self.cost, 712 "total_pnl": self.pnl, 713 } 714 715 # === Risk Metrics === 716 717 def risk_metrics( 718 self, 719 period: str = "1y", 720 risk_free_rate: float | None = None, 721 ) -> dict[str, Any]: 722 """ 723 Calculate comprehensive risk metrics. 724 725 Args: 726 period: Period for calculation (1y, 3mo, 6mo). 727 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 728 If None, uses current 10Y bond yield. 729 730 Returns: 731 Dictionary with: 732 - annualized_return: Annualized return (%) 733 - annualized_volatility: Annualized volatility (%) 734 - sharpe_ratio: Risk-adjusted return 735 - sortino_ratio: Downside risk-adjusted return 736 - max_drawdown: Maximum drawdown (%) 737 - beta: Beta vs benchmark 738 - alpha: Alpha vs benchmark (%) 739 - risk_free_rate: Risk-free rate used (%) 740 - trading_days: Number of trading days 741 """ 742 df = self.history(period=period) 743 744 if df.empty or len(df) < 20: 745 return { 746 "annualized_return": np.nan, 747 "annualized_volatility": np.nan, 748 "sharpe_ratio": np.nan, 749 "sortino_ratio": np.nan, 750 "max_drawdown": np.nan, 751 "beta": np.nan, 752 "alpha": np.nan, 753 "risk_free_rate": np.nan, 754 "trading_days": 0, 755 } 756 757 daily_returns = df["Daily_Return"].dropna() 758 trading_days = len(daily_returns) 759 annualization = 252 760 761 # Annualized return 762 total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1 763 years = trading_days / annualization 764 ann_return = ((1 + total_return) ** (1 / years) - 1) * 100 765 766 # Annualized volatility 767 daily_volatility = daily_returns.std() 768 ann_volatility = daily_volatility * np.sqrt(annualization) * 100 769 770 # Get risk-free rate 771 if risk_free_rate is None: 772 try: 773 from borsapy.bond import risk_free_rate as get_rf_rate 774 rf = get_rf_rate() * 100 # Convert to percentage 775 except Exception: 776 rf = 30.0 # Fallback 777 else: 778 rf = risk_free_rate * 100 779 780 # Sharpe Ratio 781 if ann_volatility > 0: 782 sharpe = (ann_return - rf) / ann_volatility 783 else: 784 sharpe = np.nan 785 786 # Sortino Ratio (downside deviation) 787 negative_returns = daily_returns[daily_returns < 0] 788 if len(negative_returns) > 0: 789 downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100 790 if downside_deviation > 0: 791 sortino = (ann_return - rf) / downside_deviation 792 else: 793 sortino = np.nan 794 else: 795 sortino = np.inf # No negative returns 796 797 # Maximum Drawdown 798 cumulative = (1 + daily_returns).cumprod() 799 running_max = cumulative.cummax() 800 drawdowns = (cumulative - running_max) / running_max 801 max_drawdown = drawdowns.min() * 100 802 803 # Beta and Alpha (vs benchmark) 804 beta = np.nan 805 alpha = np.nan 806 807 try: 808 bench = Index(self._benchmark) 809 bench_hist = bench.history(period=period) 810 if not bench_hist.empty: 811 bench_returns = bench_hist["Close"].pct_change().dropna() 812 813 # Align dates 814 common_dates = daily_returns.index.intersection(bench_returns.index) 815 if len(common_dates) >= 20: 816 port_ret = daily_returns.loc[common_dates] 817 bench_ret = bench_returns.loc[common_dates] 818 819 # Beta = Cov(Rp, Rm) / Var(Rm) 820 covariance = port_ret.cov(bench_ret) 821 variance = bench_ret.var() 822 if variance > 0: 823 beta = covariance / variance 824 825 # Alpha = Rp - Rf - Beta * (Rm - Rf) 826 bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1 827 bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100 828 alpha = ann_return - rf - beta * (bench_ann - rf) 829 except Exception: 830 pass 831 832 return { 833 "annualized_return": round(ann_return, 2), 834 "annualized_volatility": round(ann_volatility, 2), 835 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 836 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 837 "max_drawdown": round(max_drawdown, 2), 838 "beta": round(beta, 2) if not np.isnan(beta) else np.nan, 839 "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan, 840 "risk_free_rate": round(rf, 2), 841 "trading_days": trading_days, 842 } 843 844 def sharpe_ratio(self, period: str = "1y") -> float: 845 """ 846 Calculate Sharpe ratio. 847 848 Args: 849 period: Period for calculation. 850 851 Returns: 852 Sharpe ratio. 853 """ 854 return self.risk_metrics(period=period).get("sharpe_ratio", np.nan) 855 856 def sortino_ratio(self, period: str = "1y") -> float: 857 """ 858 Calculate Sortino ratio. 859 860 Args: 861 period: Period for calculation. 862 863 Returns: 864 Sortino ratio. 865 """ 866 return self.risk_metrics(period=period).get("sortino_ratio", np.nan) 867 868 def beta(self, benchmark: str | None = None, period: str = "1y") -> float: 869 """ 870 Calculate beta vs benchmark. 871 872 Args: 873 benchmark: Benchmark index. Uses portfolio default if None. 874 period: Period for calculation. 875 876 Returns: 877 Beta coefficient. 878 """ 879 if benchmark: 880 old_bench = self._benchmark 881 self._benchmark = benchmark 882 result = self.risk_metrics(period=period).get("beta", np.nan) 883 self._benchmark = old_bench 884 return result 885 return self.risk_metrics(period=period).get("beta", np.nan) 886 887 def correlation_matrix(self, period: str = "1y") -> pd.DataFrame: 888 """ 889 Calculate correlation matrix between holdings. 890 891 Args: 892 period: Period for calculation. 893 894 Returns: 895 DataFrame with correlation coefficients. 896 """ 897 if len(self._holdings) < 2: 898 return pd.DataFrame() 899 900 returns_dict = {} 901 for symbol, holding in self._holdings.items(): 902 try: 903 asset = self._get_or_create_asset(symbol, holding.asset_type) 904 hist = asset.history(period=period) 905 if hist.empty: 906 continue 907 price_col = "Close" if "Close" in hist.columns else "Price" 908 returns_dict[symbol] = hist[price_col].pct_change() 909 except Exception: 910 continue 911 912 if len(returns_dict) < 2: 913 return pd.DataFrame() 914 915 df = pd.DataFrame(returns_dict).dropna() 916 return df.corr() 917 918 # === Import/Export === 919 920 def to_dict(self) -> dict[str, Any]: 921 """ 922 Export portfolio to dictionary. 923 924 Returns: 925 Dictionary with portfolio data. 926 """ 927 result: dict[str, Any] = { 928 "benchmark": self._benchmark, 929 "holdings": [ 930 { 931 "symbol": h.symbol, 932 "shares": h.shares, 933 "cost_per_share": h.cost_per_share, 934 "asset_type": h.asset_type, 935 "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None, 936 } 937 for h in self._holdings.values() 938 ], 939 } 940 if self._target_weights: 941 result["target_weights"] = dict(self._target_weights) 942 return result 943 944 @classmethod 945 def from_dict(cls, data: dict[str, Any]) -> "Portfolio": 946 """ 947 Create portfolio from dictionary. 948 949 Args: 950 data: Dictionary with portfolio data. 951 952 Returns: 953 Portfolio instance. 954 """ 955 portfolio = cls(benchmark=data.get("benchmark", "XU100")) 956 for h in data.get("holdings", []): 957 # Parse purchase_date from ISO string 958 purchase_date = None 959 if h.get("purchase_date"): 960 purchase_date = date.fromisoformat(h["purchase_date"]) 961 962 portfolio.add( 963 symbol=h["symbol"], 964 shares=h["shares"], 965 cost=h.get("cost_per_share"), 966 asset_type=h.get("asset_type"), 967 purchase_date=purchase_date, 968 ) 969 # Restore target weights if present 970 if "target_weights" in data: 971 portfolio._target_weights = dict(data["target_weights"]) 972 return portfolio 973 974 # === Private Methods === 975 976 def _get_or_create_asset( 977 self, symbol: str, asset_type: AssetType 978 ) -> Ticker | FX | Crypto | Fund: 979 """Get or create asset instance from cache.""" 980 cache_key = f"{symbol}_{asset_type}" 981 if cache_key not in self._asset_cache: 982 self._asset_cache[cache_key] = _get_asset(symbol, asset_type) 983 return self._asset_cache[cache_key] 984 985 def _get_current_price(self, asset: Ticker | FX | Crypto | Fund) -> float: 986 """Get current price from asset.""" 987 try: 988 if isinstance(asset, Ticker): 989 return asset.fast_info.last_price or 0 990 elif isinstance(asset, Crypto): 991 return asset.fast_info.last_price or 0 992 elif isinstance(asset, FX): 993 current = asset.current 994 return current.get("last", 0) if current else 0 995 elif isinstance(asset, Fund): 996 info = asset.info 997 return info.get("price", 0) if info else 0 998 except Exception: 999 pass 1000 return 0 1001 1002 def __repr__(self) -> str: 1003 n = len(self._holdings) 1004 value = self.value 1005 return f"Portfolio({n} holdings, {value:,.2f} TL)" 1006 1007 def __len__(self) -> int: 1008 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 153 self._target_weights: dict[str, float] = {}
Initialize an empty portfolio.
Args: benchmark: Index symbol for beta/alpha calculations. Default is XU100 (BIST 100).
157 def add( 158 self, 159 symbol: str, 160 shares: float, 161 cost: float | None = None, 162 asset_type: str | None = None, 163 purchase_date: str | date | datetime | None = None, 164 ) -> "Portfolio": 165 """ 166 Add an asset to the portfolio. 167 168 Args: 169 symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) 170 shares: Number of shares/units. 171 cost: Cost per share/unit. If None, uses current price. 172 asset_type: Asset type override. Auto-detected if None. 173 Valid values: "stock", "fx", "crypto", "fund" 174 purchase_date: Date when the asset was purchased. 175 Accepts string (YYYY-MM-DD), date, or datetime. 176 If None, defaults to today. 177 178 Returns: 179 Self for method chaining. 180 181 Examples: 182 >>> p = Portfolio() 183 >>> p.add("THYAO", shares=100, cost=280) # Stock with cost 184 >>> p.add("GARAN", shares=200) # Stock at current price 185 >>> p.add("gram-altin", shares=5, asset_type="fx") # Metal 186 >>> p.add("YAY", shares=500, asset_type="fund") # Mutual fund 187 >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15") 188 """ 189 symbol = symbol.upper() if asset_type != "fx" else symbol 190 191 # Detect or validate asset type 192 if asset_type is None: 193 detected_type = _detect_asset_type(symbol) 194 else: 195 detected_type = asset_type # type: ignore 196 197 # Get current price if cost not provided 198 if cost is None: 199 asset = self._get_or_create_asset(symbol, detected_type) 200 cost = self._get_current_price(asset) 201 202 # Parse purchase_date 203 parsed_date: date | None = None 204 if purchase_date is not None: 205 if isinstance(purchase_date, str): 206 parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date() 207 elif isinstance(purchase_date, datetime): 208 parsed_date = purchase_date.date() 209 elif isinstance(purchase_date, date): 210 parsed_date = purchase_date 211 else: 212 parsed_date = date.today() 213 214 self._holdings[symbol] = Holding( 215 symbol=symbol, 216 shares=shares, 217 cost_per_share=cost, 218 asset_type=detected_type, 219 purchase_date=parsed_date, 220 ) 221 222 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")
224 def remove(self, symbol: str) -> "Portfolio": 225 """ 226 Remove an asset from the portfolio. 227 228 Args: 229 symbol: Asset symbol to remove. 230 231 Returns: 232 Self for method chaining. 233 """ 234 symbol_upper = symbol.upper() 235 236 # Try both original and uppercase 237 if symbol in self._holdings: 238 del self._holdings[symbol] 239 self._asset_cache.pop(symbol, None) 240 elif symbol_upper in self._holdings: 241 del self._holdings[symbol_upper] 242 self._asset_cache.pop(symbol_upper, None) 243 244 return self
Remove an asset from the portfolio.
Args: symbol: Asset symbol to remove.
Returns: Self for method chaining.
246 def update( 247 self, 248 symbol: str, 249 shares: float | None = None, 250 cost: float | None = None, 251 ) -> "Portfolio": 252 """ 253 Update an existing holding. 254 255 Args: 256 symbol: Asset symbol. 257 shares: New share count. If None, keeps existing. 258 cost: New cost per share. If None, keeps existing. 259 260 Returns: 261 Self for method chaining. 262 """ 263 if symbol not in self._holdings: 264 symbol = symbol.upper() 265 if symbol not in self._holdings: 266 raise KeyError(f"Symbol {symbol} not in portfolio") 267 268 holding = self._holdings[symbol] 269 if shares is not None: 270 holding.shares = shares 271 if cost is not None: 272 holding.cost_per_share = cost 273 274 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.
276 def clear(self) -> "Portfolio": 277 """ 278 Remove all holdings from the portfolio. 279 280 Returns: 281 Self for method chaining. 282 """ 283 self._holdings.clear() 284 self._asset_cache.clear() 285 return self
Remove all holdings from the portfolio.
Returns: Self for method chaining.
287 def set_benchmark(self, index: str) -> "Portfolio": 288 """ 289 Set the benchmark index for beta/alpha calculations. 290 291 Args: 292 index: Index symbol (XU100, XU030, XK030, etc.) 293 294 Returns: 295 Self for method chaining. 296 """ 297 self._benchmark = index 298 return self
Set the benchmark index for beta/alpha calculations.
Args: index: Index symbol (XU100, XU030, XK030, etc.)
Returns: Self for method chaining.
302 def set_target_weights(self, weights: dict[str, float]) -> "Portfolio": 303 """Set target allocation weights for rebalancing. 304 305 Args: 306 weights: Dict of symbol -> target weight (0.0 to 1.0 scale). 307 Must sum to approximately 1.0 (tolerance: 0.01). 308 309 Returns: 310 Self for method chaining. 311 312 Raises: 313 ValueError: If weights don't sum to ~1.0 or contain invalid values. 314 315 Examples: 316 >>> p = Portfolio() 317 >>> p.add("THYAO", shares=100, cost=280) 318 >>> p.add("GARAN", shares=200, cost=50) 319 >>> p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40}) 320 """ 321 total = sum(weights.values()) 322 if abs(total - 1.0) > 0.01: 323 raise ValueError( 324 f"Target weights must sum to ~1.0, got {total:.4f}" 325 ) 326 for symbol, weight in weights.items(): 327 if weight < 0 or weight > 1: 328 raise ValueError( 329 f"Weight for {symbol} must be between 0.0 and 1.0, got {weight}" 330 ) 331 self._target_weights = dict(weights) 332 return self
Set target allocation weights for rebalancing.
Args: weights: Dict of symbol -> target weight (0.0 to 1.0 scale). Must sum to approximately 1.0 (tolerance: 0.01).
Returns: Self for method chaining.
Raises: ValueError: If weights don't sum to ~1.0 or contain invalid values.
Examples:
p = Portfolio() p.add("THYAO", shares=100, cost=280) p.add("GARAN", shares=200, cost=50) p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40})
334 @property 335 def target_weights(self) -> dict[str, float]: 336 """Get current target allocation weights.""" 337 return dict(self._target_weights)
Get current target allocation weights.
339 def drift(self) -> pd.DataFrame: 340 """Calculate drift between current and target weights. 341 342 Returns: 343 DataFrame with columns: 344 - symbol: Asset symbol 345 - current_weight: Current portfolio weight (0-1 scale) 346 - target_weight: Target allocation weight (0-1 scale) 347 - drift: Absolute drift (current - target) 348 - drift_pct: Drift as percentage points 349 350 Raises: 351 ValueError: If target weights are not set. 352 """ 353 if not self._target_weights: 354 raise ValueError( 355 "Target weights not set. Call set_target_weights() first." 356 ) 357 358 current_weights = self.weights # dict[str, float] on 0-1 scale 359 rows = [] 360 361 # Include all symbols from both current holdings and targets 362 all_symbols = set(current_weights.keys()) | set(self._target_weights.keys()) 363 364 for symbol in sorted(all_symbols): 365 current = current_weights.get(symbol, 0.0) 366 target = self._target_weights.get(symbol, 0.0) 367 drift_val = current - target 368 rows.append({ 369 "symbol": symbol, 370 "current_weight": round(current, 4), 371 "target_weight": round(target, 4), 372 "drift": round(drift_val, 4), 373 "drift_pct": round(drift_val * 100, 2), 374 }) 375 376 return pd.DataFrame(rows)
Calculate drift between current and target weights.
Returns: DataFrame with columns: - symbol: Asset symbol - current_weight: Current portfolio weight (0-1 scale) - target_weight: Target allocation weight (0-1 scale) - drift: Absolute drift (current - target) - drift_pct: Drift as percentage points
Raises: ValueError: If target weights are not set.
378 def rebalance_plan(self, threshold: float = 0.0) -> pd.DataFrame: 379 """Calculate trades needed to rebalance portfolio. 380 381 Args: 382 threshold: Minimum drift (0-1 scale) to trigger a trade. 383 E.g., 0.02 = ignore drifts less than 2%. 384 385 Returns: 386 DataFrame with columns: 387 - symbol: Asset symbol 388 - current_shares: Current number of shares 389 - target_shares: Target number of shares 390 - delta_shares: Shares to buy (+) or sell (-) 391 - delta_value: Approximate trade value in TL 392 - action: "BUY", "SELL", or "HOLD" 393 394 Raises: 395 ValueError: If target weights are not set. 396 """ 397 if not self._target_weights: 398 raise ValueError( 399 "Target weights not set. Call set_target_weights() first." 400 ) 401 402 total_value = self.value 403 if total_value == 0: 404 return pd.DataFrame( 405 columns=["symbol", "current_shares", "target_shares", 406 "delta_shares", "delta_value", "action"] 407 ) 408 409 rows = [] 410 all_symbols = set(self._holdings.keys()) | set(self._target_weights.keys()) 411 412 for symbol in sorted(all_symbols): 413 holding = self._holdings.get(symbol) 414 target_weight = self._target_weights.get(symbol, 0.0) 415 416 # Get current price 417 if holding: 418 asset = self._get_or_create_asset(symbol, holding.asset_type) 419 else: 420 # Symbol in targets but not in holdings - detect type 421 detected_type = _detect_asset_type(symbol) 422 asset = self._get_or_create_asset(symbol, detected_type) 423 current_price = self._get_current_price(asset) 424 425 current_shares = holding.shares if holding else 0.0 426 current_value = current_shares * current_price 427 current_weight = current_value / total_value if total_value else 0.0 428 429 # Check threshold 430 if abs(current_weight - target_weight) < threshold: 431 rows.append({ 432 "symbol": symbol, 433 "current_shares": current_shares, 434 "target_shares": current_shares, 435 "delta_shares": 0.0, 436 "delta_value": 0.0, 437 "action": "HOLD", 438 }) 439 continue 440 441 # Calculate target shares 442 target_value = total_value * target_weight 443 if current_price > 0: 444 target_shares = target_value / current_price 445 else: 446 target_shares = 0.0 447 448 # Round stock shares to integers 449 asset_type = holding.asset_type if holding else _detect_asset_type(symbol) 450 if asset_type == "stock": 451 target_shares = round(target_shares) 452 453 delta_shares = target_shares - current_shares 454 delta_value = delta_shares * current_price 455 456 if delta_shares > 0.001: 457 action = "BUY" 458 elif delta_shares < -0.001: 459 action = "SELL" 460 else: 461 action = "HOLD" 462 463 rows.append({ 464 "symbol": symbol, 465 "current_shares": current_shares, 466 "target_shares": round(target_shares, 4), 467 "delta_shares": round(delta_shares, 4), 468 "delta_value": round(delta_value, 2), 469 "action": action, 470 }) 471 472 return pd.DataFrame(rows)
Calculate trades needed to rebalance portfolio.
Args: threshold: Minimum drift (0-1 scale) to trigger a trade. E.g., 0.02 = ignore drifts less than 2%.
Returns: DataFrame with columns: - symbol: Asset symbol - current_shares: Current number of shares - target_shares: Target number of shares - delta_shares: Shares to buy (+) or sell (-) - delta_value: Approximate trade value in TL - action: "BUY", "SELL", or "HOLD"
Raises: ValueError: If target weights are not set.
474 def rebalance(self, threshold: float = 0.0, dry_run: bool = False) -> pd.DataFrame: 475 """Execute rebalance by updating share counts. 476 477 Updates the portfolio holdings to match target weights. 478 Stock shares are rounded to integers; crypto and fund shares 479 remain fractional. 480 481 Args: 482 threshold: Minimum drift (0-1 scale) to trigger a trade. 483 dry_run: If True, return the plan without executing. 484 485 Returns: 486 DataFrame with the rebalance plan (same as rebalance_plan()). 487 488 Raises: 489 ValueError: If target weights are not set. 490 """ 491 plan = self.rebalance_plan(threshold=threshold) 492 493 if dry_run or plan.empty: 494 return plan 495 496 for _, row in plan.iterrows(): 497 symbol = row["symbol"] 498 action = row["action"] 499 target_shares = row["target_shares"] 500 501 if action == "HOLD": 502 continue 503 504 if symbol in self._holdings: 505 if target_shares <= 0: 506 self.remove(symbol) 507 else: 508 self._holdings[symbol].shares = target_shares 509 elif target_shares > 0: 510 # New holding needed 511 detected_type = _detect_asset_type(symbol) 512 asset = self._get_or_create_asset(symbol, detected_type) 513 price = self._get_current_price(asset) 514 self._holdings[symbol] = Holding( 515 symbol=symbol, 516 shares=target_shares, 517 cost_per_share=price, 518 asset_type=detected_type, 519 purchase_date=date.today(), 520 ) 521 522 return plan
Execute rebalance by updating share counts.
Updates the portfolio holdings to match target weights. Stock shares are rounded to integers; crypto and fund shares remain fractional.
Args: threshold: Minimum drift (0-1 scale) to trigger a trade. dry_run: If True, return the plan without executing.
Returns: DataFrame with the rebalance plan (same as rebalance_plan()).
Raises: ValueError: If target weights are not set.
526 @property 527 def holdings(self) -> pd.DataFrame: 528 """ 529 Get all holdings as a DataFrame. 530 531 Returns: 532 DataFrame with columns: 533 - symbol: Asset symbol 534 - shares: Number of shares 535 - cost: Cost per share 536 - current_price: Current price 537 - value: Current value (shares * price) 538 - weight: Portfolio weight (%) 539 - pnl: Profit/loss (TL) 540 - pnl_pct: Profit/loss (%) 541 - asset_type: Asset type 542 - purchase_date: Date when asset was purchased 543 - holding_days: Number of days since purchase 544 """ 545 if not self._holdings: 546 return pd.DataFrame( 547 columns=[ 548 "symbol", "shares", "cost", "current_price", 549 "value", "weight", "pnl", "pnl_pct", "asset_type", 550 "purchase_date", "holding_days" 551 ] 552 ) 553 554 rows = [] 555 total_value = self.value 556 today = date.today() 557 558 for symbol, holding in self._holdings.items(): 559 asset = self._get_or_create_asset(symbol, holding.asset_type) 560 current_price = self._get_current_price(asset) 561 value = holding.shares * current_price 562 cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0 563 pnl = value - cost_basis if cost_basis else 0 564 pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0 565 weight = (value / total_value * 100) if total_value else 0 566 567 # Calculate holding days 568 holding_days = None 569 if holding.purchase_date: 570 holding_days = (today - holding.purchase_date).days 571 572 rows.append({ 573 "symbol": symbol, 574 "shares": holding.shares, 575 "cost": holding.cost_per_share, 576 "current_price": current_price, 577 "value": value, 578 "weight": round(weight, 2), 579 "pnl": round(pnl, 2), 580 "pnl_pct": round(pnl_pct, 2), 581 "asset_type": holding.asset_type, 582 "purchase_date": holding.purchase_date, 583 "holding_days": holding_days, 584 }) 585 586 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
588 @property 589 def symbols(self) -> list[str]: 590 """Get list of symbols in portfolio.""" 591 return list(self._holdings.keys())
Get list of symbols in portfolio.
593 @property 594 def value(self) -> float: 595 """Get total portfolio value in TL.""" 596 total = 0.0 597 for symbol, holding in self._holdings.items(): 598 asset = self._get_or_create_asset(symbol, holding.asset_type) 599 price = self._get_current_price(asset) 600 total += holding.shares * price 601 return total
Get total portfolio value in TL.
603 @property 604 def cost(self) -> float: 605 """Get total portfolio cost basis in TL.""" 606 total = 0.0 607 for holding in self._holdings.values(): 608 if holding.cost_per_share: 609 total += holding.shares * holding.cost_per_share 610 return total
Get total portfolio cost basis in TL.
612 @property 613 def pnl(self) -> float: 614 """Get total profit/loss in TL.""" 615 return self.value - self.cost
Get total profit/loss in TL.
617 @property 618 def pnl_pct(self) -> float: 619 """Get total profit/loss as percentage.""" 620 cost = self.cost 621 if cost == 0: 622 return 0.0 623 return (self.pnl / cost) * 100
Get total profit/loss as percentage.
625 @property 626 def weights(self) -> dict[str, float]: 627 """Get portfolio weights as dictionary.""" 628 total_value = self.value 629 if total_value == 0: 630 return {} 631 632 result = {} 633 for symbol, holding in self._holdings.items(): 634 asset = self._get_or_create_asset(symbol, holding.asset_type) 635 price = self._get_current_price(asset) 636 value = holding.shares * price 637 result[symbol] = round(value / total_value, 4) 638 return result
Get portfolio weights as dictionary.
642 def history(self, period: str = "1y") -> pd.DataFrame: 643 """ 644 Get historical portfolio value based on current holdings. 645 646 Note: Uses current share counts - does not track historical trades. 647 When purchase_date is set for a holding, only data from that date 648 onwards is included in the portfolio value calculation. 649 650 Args: 651 period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y). 652 653 Returns: 654 DataFrame with columns: Value, Daily_Return. 655 Index is Date. 656 """ 657 if not self._holdings: 658 return pd.DataFrame(columns=["Value", "Daily_Return"]) 659 660 all_prices = {} 661 for symbol, holding in self._holdings.items(): 662 asset = self._get_or_create_asset(symbol, holding.asset_type) 663 try: 664 hist = asset.history(period=period) 665 if hist.empty: 666 continue 667 668 # Filter by purchase_date if set 669 if holding.purchase_date: 670 # Handle both timezone-aware and timezone-naive indices 671 if hasattr(hist.index, 'tz') and hist.index.tz is not None: 672 hist = hist[hist.index.date >= holding.purchase_date] 673 else: 674 hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)] 675 676 if hist.empty: 677 continue 678 679 # Use Close for stocks/index, Price for funds 680 price_col = "Close" if "Close" in hist.columns else "Price" 681 all_prices[symbol] = hist[price_col] * holding.shares 682 except Exception: 683 continue 684 685 if not all_prices: 686 return pd.DataFrame(columns=["Value", "Daily_Return"]) 687 688 df = pd.DataFrame(all_prices) 689 df = df.dropna(how="all") 690 df["Value"] = df.sum(axis=1) 691 df["Daily_Return"] = df["Value"].pct_change() 692 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.
694 @property 695 def performance(self) -> dict[str, float]: 696 """ 697 Get portfolio performance summary. 698 699 Returns: 700 Dictionary with: 701 - total_return: Total return (%) 702 - annualized_return: Annualized return (%) 703 - total_value: Current value (TL) 704 - total_cost: Total cost (TL) 705 - total_pnl: Profit/loss (TL) 706 """ 707 return { 708 "total_return": self.pnl_pct, 709 "annualized_return": np.nan, # Calculated in risk_metrics 710 "total_value": self.value, 711 "total_cost": self.cost, 712 "total_pnl": self.pnl, 713 }
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)
717 def risk_metrics( 718 self, 719 period: str = "1y", 720 risk_free_rate: float | None = None, 721 ) -> dict[str, Any]: 722 """ 723 Calculate comprehensive risk metrics. 724 725 Args: 726 period: Period for calculation (1y, 3mo, 6mo). 727 risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). 728 If None, uses current 10Y bond yield. 729 730 Returns: 731 Dictionary with: 732 - annualized_return: Annualized return (%) 733 - annualized_volatility: Annualized volatility (%) 734 - sharpe_ratio: Risk-adjusted return 735 - sortino_ratio: Downside risk-adjusted return 736 - max_drawdown: Maximum drawdown (%) 737 - beta: Beta vs benchmark 738 - alpha: Alpha vs benchmark (%) 739 - risk_free_rate: Risk-free rate used (%) 740 - trading_days: Number of trading days 741 """ 742 df = self.history(period=period) 743 744 if df.empty or len(df) < 20: 745 return { 746 "annualized_return": np.nan, 747 "annualized_volatility": np.nan, 748 "sharpe_ratio": np.nan, 749 "sortino_ratio": np.nan, 750 "max_drawdown": np.nan, 751 "beta": np.nan, 752 "alpha": np.nan, 753 "risk_free_rate": np.nan, 754 "trading_days": 0, 755 } 756 757 daily_returns = df["Daily_Return"].dropna() 758 trading_days = len(daily_returns) 759 annualization = 252 760 761 # Annualized return 762 total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1 763 years = trading_days / annualization 764 ann_return = ((1 + total_return) ** (1 / years) - 1) * 100 765 766 # Annualized volatility 767 daily_volatility = daily_returns.std() 768 ann_volatility = daily_volatility * np.sqrt(annualization) * 100 769 770 # Get risk-free rate 771 if risk_free_rate is None: 772 try: 773 from borsapy.bond import risk_free_rate as get_rf_rate 774 rf = get_rf_rate() * 100 # Convert to percentage 775 except Exception: 776 rf = 30.0 # Fallback 777 else: 778 rf = risk_free_rate * 100 779 780 # Sharpe Ratio 781 if ann_volatility > 0: 782 sharpe = (ann_return - rf) / ann_volatility 783 else: 784 sharpe = np.nan 785 786 # Sortino Ratio (downside deviation) 787 negative_returns = daily_returns[daily_returns < 0] 788 if len(negative_returns) > 0: 789 downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100 790 if downside_deviation > 0: 791 sortino = (ann_return - rf) / downside_deviation 792 else: 793 sortino = np.nan 794 else: 795 sortino = np.inf # No negative returns 796 797 # Maximum Drawdown 798 cumulative = (1 + daily_returns).cumprod() 799 running_max = cumulative.cummax() 800 drawdowns = (cumulative - running_max) / running_max 801 max_drawdown = drawdowns.min() * 100 802 803 # Beta and Alpha (vs benchmark) 804 beta = np.nan 805 alpha = np.nan 806 807 try: 808 bench = Index(self._benchmark) 809 bench_hist = bench.history(period=period) 810 if not bench_hist.empty: 811 bench_returns = bench_hist["Close"].pct_change().dropna() 812 813 # Align dates 814 common_dates = daily_returns.index.intersection(bench_returns.index) 815 if len(common_dates) >= 20: 816 port_ret = daily_returns.loc[common_dates] 817 bench_ret = bench_returns.loc[common_dates] 818 819 # Beta = Cov(Rp, Rm) / Var(Rm) 820 covariance = port_ret.cov(bench_ret) 821 variance = bench_ret.var() 822 if variance > 0: 823 beta = covariance / variance 824 825 # Alpha = Rp - Rf - Beta * (Rm - Rf) 826 bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1 827 bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100 828 alpha = ann_return - rf - beta * (bench_ann - rf) 829 except Exception: 830 pass 831 832 return { 833 "annualized_return": round(ann_return, 2), 834 "annualized_volatility": round(ann_volatility, 2), 835 "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan, 836 "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino, 837 "max_drawdown": round(max_drawdown, 2), 838 "beta": round(beta, 2) if not np.isnan(beta) else np.nan, 839 "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan, 840 "risk_free_rate": round(rf, 2), 841 "trading_days": trading_days, 842 }
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
844 def sharpe_ratio(self, period: str = "1y") -> float: 845 """ 846 Calculate Sharpe ratio. 847 848 Args: 849 period: Period for calculation. 850 851 Returns: 852 Sharpe ratio. 853 """ 854 return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)
Calculate Sharpe ratio.
Args: period: Period for calculation.
Returns: Sharpe ratio.
856 def sortino_ratio(self, period: str = "1y") -> float: 857 """ 858 Calculate Sortino ratio. 859 860 Args: 861 period: Period for calculation. 862 863 Returns: 864 Sortino ratio. 865 """ 866 return self.risk_metrics(period=period).get("sortino_ratio", np.nan)
Calculate Sortino ratio.
Args: period: Period for calculation.
Returns: Sortino ratio.
868 def beta(self, benchmark: str | None = None, period: str = "1y") -> float: 869 """ 870 Calculate beta vs benchmark. 871 872 Args: 873 benchmark: Benchmark index. Uses portfolio default if None. 874 period: Period for calculation. 875 876 Returns: 877 Beta coefficient. 878 """ 879 if benchmark: 880 old_bench = self._benchmark 881 self._benchmark = benchmark 882 result = self.risk_metrics(period=period).get("beta", np.nan) 883 self._benchmark = old_bench 884 return result 885 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.
887 def correlation_matrix(self, period: str = "1y") -> pd.DataFrame: 888 """ 889 Calculate correlation matrix between holdings. 890 891 Args: 892 period: Period for calculation. 893 894 Returns: 895 DataFrame with correlation coefficients. 896 """ 897 if len(self._holdings) < 2: 898 return pd.DataFrame() 899 900 returns_dict = {} 901 for symbol, holding in self._holdings.items(): 902 try: 903 asset = self._get_or_create_asset(symbol, holding.asset_type) 904 hist = asset.history(period=period) 905 if hist.empty: 906 continue 907 price_col = "Close" if "Close" in hist.columns else "Price" 908 returns_dict[symbol] = hist[price_col].pct_change() 909 except Exception: 910 continue 911 912 if len(returns_dict) < 2: 913 return pd.DataFrame() 914 915 df = pd.DataFrame(returns_dict).dropna() 916 return df.corr()
Calculate correlation matrix between holdings.
Args: period: Period for calculation.
Returns: DataFrame with correlation coefficients.
920 def to_dict(self) -> dict[str, Any]: 921 """ 922 Export portfolio to dictionary. 923 924 Returns: 925 Dictionary with portfolio data. 926 """ 927 result: dict[str, Any] = { 928 "benchmark": self._benchmark, 929 "holdings": [ 930 { 931 "symbol": h.symbol, 932 "shares": h.shares, 933 "cost_per_share": h.cost_per_share, 934 "asset_type": h.asset_type, 935 "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None, 936 } 937 for h in self._holdings.values() 938 ], 939 } 940 if self._target_weights: 941 result["target_weights"] = dict(self._target_weights) 942 return result
Export portfolio to dictionary.
Returns: Dictionary with portfolio data.
944 @classmethod 945 def from_dict(cls, data: dict[str, Any]) -> "Portfolio": 946 """ 947 Create portfolio from dictionary. 948 949 Args: 950 data: Dictionary with portfolio data. 951 952 Returns: 953 Portfolio instance. 954 """ 955 portfolio = cls(benchmark=data.get("benchmark", "XU100")) 956 for h in data.get("holdings", []): 957 # Parse purchase_date from ISO string 958 purchase_date = None 959 if h.get("purchase_date"): 960 purchase_date = date.fromisoformat(h["purchase_date"]) 961 962 portfolio.add( 963 symbol=h["symbol"], 964 shares=h["shares"], 965 cost=h.get("cost_per_share"), 966 asset_type=h.get("asset_type"), 967 purchase_date=purchase_date, 968 ) 969 # Restore target weights if present 970 if "target_weights" in data: 971 portfolio._target_weights = dict(data["target_weights"]) 972 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
44def banks() -> list[str]: 45 """ 46 Get list of supported banks for exchange rates. 47 48 Returns: 49 List of bank codes. 50 51 Examples: 52 >>> import borsapy as bp 53 >>> bp.banks() 54 ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...] 55 """ 56 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', ...]
59def metal_institutions() -> list[str]: 60 """ 61 Get list of supported precious metal assets for institution rates. 62 63 Returns: 64 List of asset codes that support institution_rates. 65 66 Examples: 67 >>> import borsapy as bp 68 >>> bp.metal_institutions() 69 ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin'] 70 """ 71 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']
154def crypto_pairs(quote: str = "TRY") -> list[str]: 155 """ 156 Get list of available cryptocurrency trading pairs. 157 158 Args: 159 quote: Quote currency filter (TRY, USDT, BTC) 160 161 Returns: 162 List of available trading pair symbols. 163 164 Examples: 165 >>> import borsapy as bp 166 >>> bp.crypto_pairs() 167 ['BTCTRY', 'ETHTRY', 'XRPTRY', ...] 168 >>> bp.crypto_pairs("USDT") 169 ['BTCUSDT', 'ETHUSDT', ...] 170 """ 171 provider = get_btcturk_provider() 172 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', ...]
594def search_funds(query: str, limit: int = 20) -> list[dict[str, Any]]: 595 """ 596 Search for funds by name or code. 597 598 Args: 599 query: Search query (fund code or name) 600 limit: Maximum number of results 601 602 Returns: 603 List of matching funds with fund_code, name, fund_type, return_1y. 604 605 Examples: 606 >>> import borsapy as bp 607 >>> bp.search_funds("ak portföy") 608 [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...] 609 >>> bp.search_funds("TTE") 610 [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}] 611 """ 612 provider = get_tefas_provider() 613 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...', ...}]
616def screen_funds( 617 fund_type: str = "YAT", 618 founder: str | None = None, 619 min_return_1m: float | None = None, 620 min_return_3m: float | None = None, 621 min_return_6m: float | None = None, 622 min_return_ytd: float | None = None, 623 min_return_1y: float | None = None, 624 min_return_3y: float | None = None, 625 limit: int = 50, 626) -> pd.DataFrame: 627 """ 628 Screen funds based on fund type and return criteria. 629 630 Args: 631 fund_type: Fund type filter: 632 - "YAT": Investment Funds (Yatırım Fonları) - default 633 - "EMK": Pension Funds (Emeklilik Fonları) 634 founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP") 635 min_return_1m: Minimum 1-month return (%) 636 min_return_3m: Minimum 3-month return (%) 637 min_return_6m: Minimum 6-month return (%) 638 min_return_ytd: Minimum year-to-date return (%) 639 min_return_1y: Minimum 1-year return (%) 640 min_return_3y: Minimum 3-year return (%) 641 limit: Maximum number of results (default: 50) 642 643 Returns: 644 DataFrame with funds matching the criteria, sorted by 1-year return. 645 646 Examples: 647 >>> import borsapy as bp 648 >>> bp.screen_funds(fund_type="EMK") # All pension funds 649 fund_code name return_1y ... 650 651 >>> bp.screen_funds(min_return_1y=50) # Funds with >50% 1Y return 652 fund_code name return_1y ... 653 654 >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20) 655 fund_code name return_ytd ... 656 """ 657 provider = get_tefas_provider() 658 results = provider.screen_funds( 659 fund_type=fund_type, 660 founder=founder, 661 min_return_1m=min_return_1m, 662 min_return_3m=min_return_3m, 663 min_return_6m=min_return_6m, 664 min_return_ytd=min_return_ytd, 665 min_return_1y=min_return_1y, 666 min_return_3y=min_return_3y, 667 limit=limit, 668 ) 669 670 if not results: 671 return pd.DataFrame(columns=["fund_code", "name", "fund_type", "return_1y"]) 672 673 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 ...
676def compare_funds(fund_codes: list[str]) -> dict[str, Any]: 677 """ 678 Compare multiple funds side by side. 679 680 Args: 681 fund_codes: List of TEFAS fund codes to compare (max 10) 682 683 Returns: 684 Dictionary with: 685 - funds: List of fund details with performance metrics 686 - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc) 687 - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size) 688 689 Examples: 690 >>> import borsapy as bp 691 >>> result = bp.compare_funds(["AAK", "TTE", "YAF"]) 692 >>> result['rankings']['by_return_1y'] 693 ['TTE', 'YAF', 'AAK'] 694 695 >>> result['summary'] 696 {'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...} 697 698 >>> for fund in result['funds']: 699 ... print(f"{fund['fund_code']}: {fund['return_1y']}%") 700 AAK: 32.5% 701 TTE: 72.1% 702 YAF: 31.0% 703 """ 704 provider = get_tefas_provider() 705 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%
708def management_fees( 709 fund_type: str = "YAT", 710 founder: str | None = None, 711) -> pd.DataFrame: 712 """ 713 Get management fee data for all funds. 714 715 Args: 716 fund_type: Fund type filter: 717 - "YAT": Investment Funds (Yatırım Fonları) - default 718 - "EMK": Pension Funds (Emeklilik Fonları) 719 founder: Filter by founder company code (e.g., "AKP", "GPY") 720 721 Returns: 722 DataFrame with columns: fund_code, name, fund_category, founder_code, 723 applied_fee, prospectus_fee, max_expense_ratio, annual_return. 724 725 Examples: 726 >>> import borsapy as bp 727 >>> df = bp.management_fees() 728 >>> df = bp.management_fees(fund_type="EMK") 729 >>> df = bp.management_fees(founder="AKP") 730 """ 731 provider = get_tefas_provider() 732 results = provider.get_management_fees(fund_type=fund_type, founder=founder) 733 734 if not results: 735 return pd.DataFrame(columns=[ 736 "fund_code", "name", "fund_category", "founder_code", 737 "applied_fee", "prospectus_fee", "max_expense_ratio", "annual_return", 738 ]) 739 740 return pd.DataFrame(results)
Get management fee data for all funds.
Args: fund_type: Fund type filter: - "YAT": Investment Funds (Yatırım Fonları) - default - "EMK": Pension Funds (Emeklilik Fonları) founder: Filter by founder company code (e.g., "AKP", "GPY")
Returns: DataFrame with columns: fund_code, name, fund_category, founder_code, applied_fee, prospectus_fee, max_expense_ratio, annual_return.
Examples:
import borsapy as bp df = bp.management_fees() df = bp.management_fees(fund_type="EMK") df = bp.management_fees(founder="AKP")
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
855class TechnicalAnalyzer: 856 """Technical analysis wrapper for OHLCV DataFrames. 857 858 Provides easy access to technical indicators as methods and properties. 859 860 Example: 861 >>> df = stock.history(period="1y") 862 >>> ta = TechnicalAnalyzer(df) 863 >>> ta.rsi() # Returns full RSI series 864 >>> ta.latest # Returns dict with latest values of all indicators 865 """ 866 867 def __init__(self, df: pd.DataFrame) -> None: 868 """Initialize with OHLCV DataFrame. 869 870 Args: 871 df: DataFrame with price data (must have at least 'Close' column) 872 """ 873 self._df = df.copy() 874 self._has_volume = "Volume" in df.columns 875 self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"]) 876 877 def sma(self, period: int = 20) -> pd.Series: 878 """Calculate Simple Moving Average.""" 879 return calculate_sma(self._df, period) 880 881 def ema(self, period: int = 20) -> pd.Series: 882 """Calculate Exponential Moving Average.""" 883 return calculate_ema(self._df, period) 884 885 def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series: 886 """Calculate Tilson T3 Moving Average.""" 887 return calculate_tilson_t3(self._df, period, vfactor) 888 889 def rsi(self, period: int = 14) -> pd.Series: 890 """Calculate Relative Strength Index.""" 891 return calculate_rsi(self._df, period) 892 893 def macd( 894 self, fast: int = 12, slow: int = 26, signal: int = 9 895 ) -> pd.DataFrame: 896 """Calculate MACD (line, signal, histogram).""" 897 return calculate_macd(self._df, fast, slow, signal) 898 899 def bollinger_bands( 900 self, period: int = 20, std_dev: float = 2.0 901 ) -> pd.DataFrame: 902 """Calculate Bollinger Bands (upper, middle, lower).""" 903 return calculate_bollinger_bands(self._df, period, std_dev) 904 905 def atr(self, period: int = 14) -> pd.Series: 906 """Calculate Average True Range.""" 907 return calculate_atr(self._df, period) 908 909 def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame: 910 """Calculate Stochastic Oscillator (%K, %D).""" 911 return calculate_stochastic(self._df, k_period, d_period) 912 913 def obv(self) -> pd.Series: 914 """Calculate On-Balance Volume.""" 915 return calculate_obv(self._df) 916 917 def vwap(self) -> pd.Series: 918 """Calculate Volume Weighted Average Price.""" 919 return calculate_vwap(self._df) 920 921 def adx(self, period: int = 14) -> pd.Series: 922 """Calculate Average Directional Index.""" 923 return calculate_adx(self._df, period) 924 925 def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame: 926 """Calculate Supertrend indicator. 927 928 Args: 929 atr_period: Period for ATR calculation (default 10) 930 multiplier: ATR multiplier for bands (default 3.0) 931 932 Returns: 933 DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower 934 """ 935 return calculate_supertrend(self._df, atr_period, multiplier) 936 937 def hhv(self, period: int = 14, column: str = "High") -> pd.Series: 938 """Calculate Highest High Value (HHV).""" 939 return calculate_hhv(self._df, period, column) 940 941 def llv(self, period: int = 14, column: str = "Low") -> pd.Series: 942 """Calculate Lowest Low Value (LLV).""" 943 return calculate_llv(self._df, period, column) 944 945 def mom(self, period: int = 10) -> pd.Series: 946 """Calculate Momentum (MOM).""" 947 return calculate_mom(self._df, period) 948 949 def roc(self, period: int = 10) -> pd.Series: 950 """Calculate Rate of Change (ROC).""" 951 return calculate_roc(self._df, period) 952 953 def wma(self, period: int = 20) -> pd.Series: 954 """Calculate Weighted Moving Average (WMA).""" 955 return calculate_wma(self._df, period) 956 957 def dema(self, period: int = 20) -> pd.Series: 958 """Calculate Double Exponential Moving Average (DEMA).""" 959 return calculate_dema(self._df, period) 960 961 def tema(self, period: int = 20) -> pd.Series: 962 """Calculate Triple Exponential Moving Average (TEMA).""" 963 return calculate_tema(self._df, period) 964 965 def heikin_ashi(self) -> pd.DataFrame: 966 """Calculate Heikin Ashi candlestick values. 967 968 Returns: 969 DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns 970 """ 971 from borsapy.charts import calculate_heikin_ashi 972 973 return calculate_heikin_ashi(self._df) 974 975 def all(self, **kwargs: Any) -> pd.DataFrame: 976 """Get DataFrame with all applicable indicators added.""" 977 return add_indicators(self._df, **kwargs) 978 979 @property 980 def latest(self) -> dict[str, float]: 981 """Get latest values of all applicable indicators. 982 983 Returns: 984 Dictionary with indicator names and their latest values 985 """ 986 result: dict[str, float] = {} 987 988 # Always available (need Close or Price) 989 has_price = "Close" in self._df.columns or "Price" in self._df.columns 990 if has_price and len(self._df) > 0: 991 result["sma_20"] = float(self.sma(20).iloc[-1]) 992 result["sma_50"] = float(self.sma(50).iloc[-1]) 993 result["ema_12"] = float(self.ema(12).iloc[-1]) 994 result["ema_26"] = float(self.ema(26).iloc[-1]) 995 result["t3_5"] = float(self.tilson_t3(5).iloc[-1]) 996 result["rsi_14"] = float(self.rsi(14).iloc[-1]) 997 998 macd_df = self.macd() 999 result["macd"] = float(macd_df["MACD"].iloc[-1]) 1000 result["macd_signal"] = float(macd_df["Signal"].iloc[-1]) 1001 result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1]) 1002 1003 bb_df = self.bollinger_bands() 1004 result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1]) 1005 result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1]) 1006 result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1]) 1007 1008 # MetaStock indicators (need Close or Price) 1009 if has_price and len(self._df) > 0: 1010 result["mom_10"] = float(self.mom(10).iloc[-1]) 1011 result["roc_10"] = float(self.roc(10).iloc[-1]) 1012 result["wma_20"] = float(self.wma(20).iloc[-1]) 1013 result["dema_20"] = float(self.dema(20).iloc[-1]) 1014 result["tema_20"] = float(self.tema(20).iloc[-1]) 1015 1016 # Need High, Low, Close 1017 if self._has_hlc and len(self._df) > 0: 1018 result["atr_14"] = float(self.atr(14).iloc[-1]) 1019 result["adx_14"] = float(self.adx(14).iloc[-1]) 1020 result["hhv_14"] = float(self.hhv(14).iloc[-1]) 1021 result["llv_14"] = float(self.llv(14).iloc[-1]) 1022 1023 stoch_df = self.stochastic() 1024 result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1]) 1025 result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1]) 1026 1027 st_df = self.supertrend() 1028 result["supertrend"] = float(st_df["Supertrend"].iloc[-1]) 1029 result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1]) 1030 1031 # Need Volume 1032 if self._has_volume and len(self._df) > 0: 1033 result["obv"] = float(self.obv().iloc[-1]) 1034 1035 # Need HLC + Volume 1036 if self._has_hlc and self._has_volume and len(self._df) > 0: 1037 result["vwap"] = float(self.vwap().iloc[-1]) 1038 1039 # Round all values 1040 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
867 def __init__(self, df: pd.DataFrame) -> None: 868 """Initialize with OHLCV DataFrame. 869 870 Args: 871 df: DataFrame with price data (must have at least 'Close' column) 872 """ 873 self._df = df.copy() 874 self._has_volume = "Volume" in df.columns 875 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)
877 def sma(self, period: int = 20) -> pd.Series: 878 """Calculate Simple Moving Average.""" 879 return calculate_sma(self._df, period)
Calculate Simple Moving Average.
881 def ema(self, period: int = 20) -> pd.Series: 882 """Calculate Exponential Moving Average.""" 883 return calculate_ema(self._df, period)
Calculate Exponential Moving Average.
885 def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series: 886 """Calculate Tilson T3 Moving Average.""" 887 return calculate_tilson_t3(self._df, period, vfactor)
Calculate Tilson T3 Moving Average.
889 def rsi(self, period: int = 14) -> pd.Series: 890 """Calculate Relative Strength Index.""" 891 return calculate_rsi(self._df, period)
Calculate Relative Strength Index.
893 def macd( 894 self, fast: int = 12, slow: int = 26, signal: int = 9 895 ) -> pd.DataFrame: 896 """Calculate MACD (line, signal, histogram).""" 897 return calculate_macd(self._df, fast, slow, signal)
Calculate MACD (line, signal, histogram).
899 def bollinger_bands( 900 self, period: int = 20, std_dev: float = 2.0 901 ) -> pd.DataFrame: 902 """Calculate Bollinger Bands (upper, middle, lower).""" 903 return calculate_bollinger_bands(self._df, period, std_dev)
Calculate Bollinger Bands (upper, middle, lower).
905 def atr(self, period: int = 14) -> pd.Series: 906 """Calculate Average True Range.""" 907 return calculate_atr(self._df, period)
Calculate Average True Range.
909 def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame: 910 """Calculate Stochastic Oscillator (%K, %D).""" 911 return calculate_stochastic(self._df, k_period, d_period)
Calculate Stochastic Oscillator (%K, %D).
913 def obv(self) -> pd.Series: 914 """Calculate On-Balance Volume.""" 915 return calculate_obv(self._df)
Calculate On-Balance Volume.
917 def vwap(self) -> pd.Series: 918 """Calculate Volume Weighted Average Price.""" 919 return calculate_vwap(self._df)
Calculate Volume Weighted Average Price.
921 def adx(self, period: int = 14) -> pd.Series: 922 """Calculate Average Directional Index.""" 923 return calculate_adx(self._df, period)
Calculate Average Directional Index.
925 def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame: 926 """Calculate Supertrend indicator. 927 928 Args: 929 atr_period: Period for ATR calculation (default 10) 930 multiplier: ATR multiplier for bands (default 3.0) 931 932 Returns: 933 DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower 934 """ 935 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
937 def hhv(self, period: int = 14, column: str = "High") -> pd.Series: 938 """Calculate Highest High Value (HHV).""" 939 return calculate_hhv(self._df, period, column)
Calculate Highest High Value (HHV).
941 def llv(self, period: int = 14, column: str = "Low") -> pd.Series: 942 """Calculate Lowest Low Value (LLV).""" 943 return calculate_llv(self._df, period, column)
Calculate Lowest Low Value (LLV).
945 def mom(self, period: int = 10) -> pd.Series: 946 """Calculate Momentum (MOM).""" 947 return calculate_mom(self._df, period)
Calculate Momentum (MOM).
949 def roc(self, period: int = 10) -> pd.Series: 950 """Calculate Rate of Change (ROC).""" 951 return calculate_roc(self._df, period)
Calculate Rate of Change (ROC).
953 def wma(self, period: int = 20) -> pd.Series: 954 """Calculate Weighted Moving Average (WMA).""" 955 return calculate_wma(self._df, period)
Calculate Weighted Moving Average (WMA).
957 def dema(self, period: int = 20) -> pd.Series: 958 """Calculate Double Exponential Moving Average (DEMA).""" 959 return calculate_dema(self._df, period)
Calculate Double Exponential Moving Average (DEMA).
961 def tema(self, period: int = 20) -> pd.Series: 962 """Calculate Triple Exponential Moving Average (TEMA).""" 963 return calculate_tema(self._df, period)
Calculate Triple Exponential Moving Average (TEMA).
965 def heikin_ashi(self) -> pd.DataFrame: 966 """Calculate Heikin Ashi candlestick values. 967 968 Returns: 969 DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns 970 """ 971 from borsapy.charts import calculate_heikin_ashi 972 973 return calculate_heikin_ashi(self._df)
Calculate Heikin Ashi candlestick values.
Returns: DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
975 def all(self, **kwargs: Any) -> pd.DataFrame: 976 """Get DataFrame with all applicable indicators added.""" 977 return add_indicators(self._df, **kwargs)
Get DataFrame with all applicable indicators added.
979 @property 980 def latest(self) -> dict[str, float]: 981 """Get latest values of all applicable indicators. 982 983 Returns: 984 Dictionary with indicator names and their latest values 985 """ 986 result: dict[str, float] = {} 987 988 # Always available (need Close or Price) 989 has_price = "Close" in self._df.columns or "Price" in self._df.columns 990 if has_price and len(self._df) > 0: 991 result["sma_20"] = float(self.sma(20).iloc[-1]) 992 result["sma_50"] = float(self.sma(50).iloc[-1]) 993 result["ema_12"] = float(self.ema(12).iloc[-1]) 994 result["ema_26"] = float(self.ema(26).iloc[-1]) 995 result["t3_5"] = float(self.tilson_t3(5).iloc[-1]) 996 result["rsi_14"] = float(self.rsi(14).iloc[-1]) 997 998 macd_df = self.macd() 999 result["macd"] = float(macd_df["MACD"].iloc[-1]) 1000 result["macd_signal"] = float(macd_df["Signal"].iloc[-1]) 1001 result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1]) 1002 1003 bb_df = self.bollinger_bands() 1004 result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1]) 1005 result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1]) 1006 result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1]) 1007 1008 # MetaStock indicators (need Close or Price) 1009 if has_price and len(self._df) > 0: 1010 result["mom_10"] = float(self.mom(10).iloc[-1]) 1011 result["roc_10"] = float(self.roc(10).iloc[-1]) 1012 result["wma_20"] = float(self.wma(20).iloc[-1]) 1013 result["dema_20"] = float(self.dema(20).iloc[-1]) 1014 result["tema_20"] = float(self.tema(20).iloc[-1]) 1015 1016 # Need High, Low, Close 1017 if self._has_hlc and len(self._df) > 0: 1018 result["atr_14"] = float(self.atr(14).iloc[-1]) 1019 result["adx_14"] = float(self.adx(14).iloc[-1]) 1020 result["hhv_14"] = float(self.hhv(14).iloc[-1]) 1021 result["llv_14"] = float(self.llv(14).iloc[-1]) 1022 1023 stoch_df = self.stochastic() 1024 result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1]) 1025 result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1]) 1026 1027 st_df = self.supertrend() 1028 result["supertrend"] = float(st_df["Supertrend"].iloc[-1]) 1029 result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1]) 1030 1031 # Need Volume 1032 if self._has_volume and len(self._df) > 0: 1033 result["obv"] = float(self.obv().iloc[-1]) 1034 1035 # Need HLC + Volume 1036 if self._has_hlc and self._has_volume and len(self._df) > 0: 1037 result["vwap"] = float(self.vwap().iloc[-1]) 1038 1039 # Round all values 1040 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
729def add_indicators( 730 df: pd.DataFrame, 731 indicators: list[str] | None = None, 732 **kwargs: Any, 733) -> pd.DataFrame: 734 """Add technical indicator columns to a DataFrame. 735 736 Args: 737 df: DataFrame with OHLCV data (Open, High, Low, Close, Volume) 738 indicators: List of indicators to add. If None, adds all applicable. 739 Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr', 740 'stochastic', 'obv', 'vwap', 'adx', 'supertrend', 741 'hhv', 'llv', 'mom', 'roc', 'wma', 'dema', 'tema' 742 **kwargs: Additional arguments for specific indicators: 743 - sma_period: SMA period (default 20) 744 - ema_period: EMA period (default 12) 745 - rsi_period: RSI period (default 14) 746 - bb_period: Bollinger Bands period (default 20) 747 - atr_period: ATR period (default 14) 748 - adx_period: ADX period (default 14) 749 - supertrend_period: Supertrend ATR period (default 10) 750 - supertrend_multiplier: Supertrend ATR multiplier (default 3.0) 751 - hhv_period: HHV period (default 14) 752 - llv_period: LLV period (default 14) 753 - mom_period: MOM period (default 10) 754 - roc_period: ROC period (default 10) 755 - wma_period: WMA period (default 20) 756 - dema_period: DEMA period (default 20) 757 - tema_period: TEMA period (default 20) 758 759 Returns: 760 DataFrame with indicator columns added 761 """ 762 result = df.copy() 763 764 # Default indicators based on available columns 765 has_volume = "Volume" in df.columns 766 has_hlc = all(col in df.columns for col in ["High", "Low", "Close"]) 767 768 if indicators is None: 769 indicators = ["sma", "ema", "rsi", "macd", "bollinger"] 770 if has_hlc: 771 indicators.extend(["atr", "stochastic", "adx", "supertrend"]) 772 if has_volume: 773 indicators.append("obv") 774 if has_volume and has_hlc: 775 indicators.append("vwap") 776 777 # Get periods from kwargs 778 sma_period = kwargs.get("sma_period", 20) 779 ema_period = kwargs.get("ema_period", 12) 780 rsi_period = kwargs.get("rsi_period", 14) 781 bb_period = kwargs.get("bb_period", 20) 782 atr_period = kwargs.get("atr_period", 14) 783 adx_period = kwargs.get("adx_period", 14) 784 supertrend_period = kwargs.get("supertrend_period", 10) 785 supertrend_multiplier = kwargs.get("supertrend_multiplier", 3.0) 786 hhv_period = kwargs.get("hhv_period", 14) 787 llv_period = kwargs.get("llv_period", 14) 788 mom_period = kwargs.get("mom_period", 10) 789 roc_period = kwargs.get("roc_period", 10) 790 wma_period = kwargs.get("wma_period", 20) 791 dema_period = kwargs.get("dema_period", 20) 792 tema_period = kwargs.get("tema_period", 20) 793 794 # Add indicators 795 for indicator in indicators: 796 indicator = indicator.lower() 797 798 if indicator == "sma": 799 result[f"SMA_{sma_period}"] = calculate_sma(df, sma_period) 800 elif indicator == "ema": 801 result[f"EMA_{ema_period}"] = calculate_ema(df, ema_period) 802 elif indicator == "rsi": 803 result[f"RSI_{rsi_period}"] = calculate_rsi(df, rsi_period) 804 elif indicator == "macd": 805 macd_df = calculate_macd(df) 806 result["MACD"] = macd_df["MACD"] 807 result["MACD_Signal"] = macd_df["Signal"] 808 result["MACD_Hist"] = macd_df["Histogram"] 809 elif indicator == "bollinger": 810 bb_df = calculate_bollinger_bands(df, bb_period) 811 result["BB_Upper"] = bb_df["BB_Upper"] 812 result["BB_Middle"] = bb_df["BB_Middle"] 813 result["BB_Lower"] = bb_df["BB_Lower"] 814 elif indicator == "atr" and has_hlc: 815 result[f"ATR_{atr_period}"] = calculate_atr(df, atr_period) 816 elif indicator == "stochastic" and has_hlc: 817 stoch_df = calculate_stochastic(df) 818 result["Stoch_K"] = stoch_df["Stoch_K"] 819 result["Stoch_D"] = stoch_df["Stoch_D"] 820 elif indicator == "obv" and has_volume: 821 result["OBV"] = calculate_obv(df) 822 elif indicator == "vwap" and has_volume and has_hlc: 823 result["VWAP"] = calculate_vwap(df) 824 elif indicator == "adx" and has_hlc: 825 result[f"ADX_{adx_period}"] = calculate_adx(df, adx_period) 826 elif indicator == "supertrend" and has_hlc: 827 st_df = calculate_supertrend(df, supertrend_period, supertrend_multiplier) 828 result["Supertrend"] = st_df["Supertrend"] 829 result["Supertrend_Direction"] = st_df["Supertrend_Direction"] 830 elif indicator == "hhv": 831 col = "High" if "High" in df.columns else "Close" 832 result[f"HHV_{hhv_period}"] = calculate_hhv(df, hhv_period, col) 833 elif indicator == "llv": 834 col = "Low" if "Low" in df.columns else "Close" 835 result[f"LLV_{llv_period}"] = calculate_llv(df, llv_period, col) 836 elif indicator == "mom": 837 result[f"MOM_{mom_period}"] = calculate_mom(df, mom_period) 838 elif indicator == "roc": 839 result[f"ROC_{roc_period}"] = calculate_roc(df, roc_period) 840 elif indicator == "wma": 841 result[f"WMA_{wma_period}"] = calculate_wma(df, wma_period) 842 elif indicator == "dema": 843 result[f"DEMA_{dema_period}"] = calculate_dema(df, dema_period) 844 elif indicator == "tema": 845 result[f"TEMA_{tema_period}"] = calculate_tema(df, tema_period) 846 847 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', 'hhv', 'llv', 'mom', 'roc', 'wma', 'dema', 'tema' **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) - hhv_period: HHV period (default 14) - llv_period: LLV period (default 14) - mom_period: MOM period (default 10) - roc_period: ROC period (default 10) - wma_period: WMA period (default 20) - dema_period: DEMA period (default 20) - tema_period: TEMA period (default 20)
Returns: DataFrame with indicator columns added
62def calculate_sma( 63 df: pd.DataFrame, period: int = 20, column: str = "Close" 64) -> pd.Series: 65 """Calculate Simple Moving Average (SMA). 66 67 Args: 68 df: DataFrame with price data 69 period: Number of periods for moving average 70 column: Column name to use for calculation 71 72 Returns: 73 Series with SMA values 74 """ 75 col = _get_price_column(df, column) 76 if col not in df.columns: 77 return pd.Series(np.nan, index=df.index, name=f"SMA_{period}") 78 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
81def calculate_ema( 82 df: pd.DataFrame, period: int = 20, column: str = "Close" 83) -> pd.Series: 84 """Calculate Exponential Moving Average (EMA). 85 86 Args: 87 df: DataFrame with price data 88 period: Number of periods for moving average 89 column: Column name to use for calculation 90 91 Returns: 92 Series with EMA values 93 """ 94 col = _get_price_column(df, column) 95 if col not in df.columns: 96 return pd.Series(np.nan, index=df.index, name=f"EMA_{period}") 97 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
157def calculate_rsi( 158 df: pd.DataFrame, period: int = 14, column: str = "Close" 159) -> pd.Series: 160 """Calculate Relative Strength Index (RSI). 161 162 RSI measures the speed and magnitude of price movements on a scale of 0-100. 163 - RSI > 70: Overbought (potential sell signal) 164 - RSI < 30: Oversold (potential buy signal) 165 166 Args: 167 df: DataFrame with price data 168 period: Number of periods for RSI calculation (default 14) 169 column: Column name to use for calculation 170 171 Returns: 172 Series with RSI values (0-100) 173 """ 174 col = _get_price_column(df, column) 175 if col not in df.columns or len(df) < period: 176 return pd.Series(np.nan, index=df.index, name=f"RSI_{period}") 177 178 delta = df[col].diff() 179 gain = delta.where(delta > 0, 0.0) 180 loss = (-delta).where(delta < 0, 0.0) 181 182 # Use Wilder's smoothing (same as TradingView) 183 # Wilder's uses alpha=1/period, NOT span=period 184 avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() 185 avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() 186 187 rs = avg_gain / avg_loss 188 rsi = 100.0 - (100.0 / (1.0 + rs)) 189 190 # Handle division by zero 191 rsi = rsi.replace([np.inf, -np.inf], np.nan) 192 rsi = rsi.fillna(50.0) # Neutral RSI when no movement 193 194 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)
197def calculate_macd( 198 df: pd.DataFrame, 199 fast: int = 12, 200 slow: int = 26, 201 signal: int = 9, 202 column: str = "Close", 203) -> pd.DataFrame: 204 """Calculate Moving Average Convergence Divergence (MACD). 205 206 MACD shows the relationship between two moving averages of prices. 207 - MACD Line: Fast EMA - Slow EMA 208 - Signal Line: EMA of MACD Line 209 - Histogram: MACD Line - Signal Line 210 211 Args: 212 df: DataFrame with price data 213 fast: Fast EMA period (default 12) 214 slow: Slow EMA period (default 26) 215 signal: Signal line EMA period (default 9) 216 column: Column name to use for calculation 217 218 Returns: 219 DataFrame with columns: MACD, Signal, Histogram 220 """ 221 col = _get_price_column(df, column) 222 if col not in df.columns: 223 return pd.DataFrame( 224 {"MACD": np.nan, "Signal": np.nan, "Histogram": np.nan}, 225 index=df.index, 226 ) 227 228 ema_fast = df[col].ewm(span=fast, adjust=False).mean() 229 ema_slow = df[col].ewm(span=slow, adjust=False).mean() 230 231 macd_line = ema_fast - ema_slow 232 signal_line = macd_line.ewm(span=signal, adjust=False).mean() 233 histogram = macd_line - signal_line 234 235 return pd.DataFrame( 236 {"MACD": macd_line, "Signal": signal_line, "Histogram": histogram}, 237 index=df.index, 238 )
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
241def calculate_bollinger_bands( 242 df: pd.DataFrame, period: int = 20, std_dev: float = 2.0, column: str = "Close" 243) -> pd.DataFrame: 244 """Calculate Bollinger Bands. 245 246 Bollinger Bands consist of a middle band (SMA) and two outer bands 247 at standard deviation levels above and below the middle band. 248 249 Args: 250 df: DataFrame with price data 251 period: Period for SMA and standard deviation 252 std_dev: Number of standard deviations for bands 253 column: Column name to use for calculation 254 255 Returns: 256 DataFrame with columns: Upper, Middle, Lower 257 """ 258 col = _get_price_column(df, column) 259 if col not in df.columns: 260 return pd.DataFrame( 261 {"BB_Upper": np.nan, "BB_Middle": np.nan, "BB_Lower": np.nan}, 262 index=df.index, 263 ) 264 265 middle = df[col].rolling(window=period, min_periods=1).mean() 266 std = df[col].rolling(window=period, min_periods=1).std() 267 268 upper = middle + (std * std_dev) 269 lower = middle - (std * std_dev) 270 271 return pd.DataFrame( 272 {"BB_Upper": upper, "BB_Middle": middle, "BB_Lower": lower}, 273 index=df.index, 274 )
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
277def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series: 278 """Calculate Average True Range (ATR). 279 280 ATR measures market volatility by decomposing the entire range of an asset 281 price for that period. 282 283 Args: 284 df: DataFrame with High, Low, Close columns 285 period: Period for ATR calculation 286 287 Returns: 288 Series with ATR values 289 """ 290 required = ["High", "Low", "Close"] 291 if not all(col in df.columns for col in required): 292 return pd.Series(np.nan, index=df.index, name=f"ATR_{period}") 293 294 high = df["High"] 295 low = df["Low"] 296 close = df["Close"] 297 298 # True Range components 299 tr1 = high - low 300 tr2 = abs(high - close.shift(1)) 301 tr3 = abs(low - close.shift(1)) 302 303 # True Range is the maximum of the three 304 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 305 306 # ATR uses Wilder's smoothing (same as TradingView) 307 atr = tr.ewm(alpha=1 / period, adjust=False).mean() 308 309 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
312def calculate_stochastic( 313 df: pd.DataFrame, k_period: int = 14, d_period: int = 3 314) -> pd.DataFrame: 315 """Calculate Stochastic Oscillator (%K and %D). 316 317 The Stochastic Oscillator compares a closing price to a range of prices 318 over a certain period of time. 319 - %K > 80: Overbought 320 - %K < 20: Oversold 321 322 Args: 323 df: DataFrame with High, Low, Close columns 324 k_period: Period for %K calculation 325 d_period: Period for %D (signal line) 326 327 Returns: 328 DataFrame with columns: Stoch_K, Stoch_D 329 """ 330 required = ["High", "Low", "Close"] 331 if not all(col in df.columns for col in required): 332 return pd.DataFrame( 333 {"Stoch_K": np.nan, "Stoch_D": np.nan}, 334 index=df.index, 335 ) 336 337 # Calculate %K 338 lowest_low = df["Low"].rolling(window=k_period, min_periods=1).min() 339 highest_high = df["High"].rolling(window=k_period, min_periods=1).max() 340 341 stoch_k = 100 * (df["Close"] - lowest_low) / (highest_high - lowest_low) 342 stoch_k = stoch_k.replace([np.inf, -np.inf], np.nan).fillna(50.0) 343 344 # %D is the SMA of %K 345 stoch_d = stoch_k.rolling(window=d_period, min_periods=1).mean() 346 347 return pd.DataFrame( 348 {"Stoch_K": stoch_k, "Stoch_D": stoch_d}, 349 index=df.index, 350 )
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
353def calculate_obv(df: pd.DataFrame) -> pd.Series: 354 """Calculate On-Balance Volume (OBV). 355 356 OBV uses volume flow to predict changes in stock price. 357 Rising OBV indicates positive volume pressure that can lead to higher prices. 358 359 Args: 360 df: DataFrame with Close and Volume columns 361 362 Returns: 363 Series with OBV values 364 """ 365 required = ["Close", "Volume"] 366 if not all(col in df.columns for col in required): 367 return pd.Series(np.nan, index=df.index, name="OBV") 368 369 # Direction: +1 if close > previous close, -1 if close < previous close, 0 if equal 370 direction = np.sign(df["Close"].diff()) 371 direction.iloc[0] = 0 # First value has no direction 372 373 # OBV is cumulative sum of signed volume 374 obv = (direction * df["Volume"]).cumsum() 375 376 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
379def calculate_vwap(df: pd.DataFrame) -> pd.Series: 380 """Calculate Volume Weighted Average Price (VWAP). 381 382 VWAP gives the average price weighted by volume. 383 It's often used as a trading benchmark. 384 385 Args: 386 df: DataFrame with High, Low, Close, Volume columns 387 388 Returns: 389 Series with VWAP values 390 """ 391 required = ["High", "Low", "Close", "Volume"] 392 if not all(col in df.columns for col in required): 393 return pd.Series(np.nan, index=df.index, name="VWAP") 394 395 # Typical Price 396 typical_price = (df["High"] + df["Low"] + df["Close"]) / 3 397 398 # Cumulative TP * Volume / Cumulative Volume 399 cumulative_tp_vol = (typical_price * df["Volume"]).cumsum() 400 cumulative_vol = df["Volume"].cumsum() 401 402 vwap = cumulative_tp_vol / cumulative_vol 403 vwap = vwap.replace([np.inf, -np.inf], np.nan) 404 405 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
408def calculate_adx(df: pd.DataFrame, period: int = 14) -> pd.Series: 409 """Calculate Average Directional Index (ADX). 410 411 ADX measures the strength of a trend regardless of its direction. 412 - ADX > 25: Strong trend 413 - ADX < 20: Weak or no trend 414 415 Args: 416 df: DataFrame with High, Low, Close columns 417 period: Period for ADX calculation 418 419 Returns: 420 Series with ADX values 421 """ 422 required = ["High", "Low", "Close"] 423 if not all(col in df.columns for col in required): 424 return pd.Series(np.nan, index=df.index, name=f"ADX_{period}") 425 426 high = df["High"] 427 low = df["Low"] 428 close = df["Close"] 429 430 # Calculate +DM and -DM 431 plus_dm = high.diff() 432 minus_dm = -low.diff() 433 434 plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0.0) 435 minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0.0) 436 437 # True Range 438 tr1 = high - low 439 tr2 = abs(high - close.shift(1)) 440 tr3 = abs(low - close.shift(1)) 441 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 442 443 # Smoothed values using Wilder's smoothing (same as TradingView) 444 atr = tr.ewm(alpha=1 / period, adjust=False).mean() 445 plus_di = 100 * (plus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr) 446 minus_di = 100 * (minus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr) 447 448 # DX and ADX 449 dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di) 450 dx = dx.replace([np.inf, -np.inf], np.nan).fillna(0) 451 452 adx = dx.ewm(alpha=1 / period, adjust=False).mean() 453 454 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
457def calculate_supertrend( 458 df: pd.DataFrame, atr_period: int = 10, multiplier: float = 3.0 459) -> pd.DataFrame: 460 """Calculate Supertrend indicator. 461 462 Supertrend is a trend-following indicator based on ATR. 463 - When price is above Supertrend line: Bullish (uptrend) 464 - When price is below Supertrend line: Bearish (downtrend) 465 466 Args: 467 df: DataFrame with High, Low, Close columns 468 atr_period: Period for ATR calculation (default: 10) 469 multiplier: ATR multiplier for bands (default: 3.0) 470 471 Returns: 472 DataFrame with columns: 473 - Supertrend: The Supertrend line value 474 - Supertrend_Direction: 1 for bullish, -1 for bearish 475 - Supertrend_Upper: Upper band 476 - Supertrend_Lower: Lower band 477 """ 478 required = ["High", "Low", "Close"] 479 if not all(col in df.columns for col in required): 480 return pd.DataFrame( 481 { 482 "Supertrend": np.nan, 483 "Supertrend_Direction": np.nan, 484 "Supertrend_Upper": np.nan, 485 "Supertrend_Lower": np.nan, 486 }, 487 index=df.index, 488 ) 489 490 high = df["High"] 491 low = df["Low"] 492 close = df["Close"] 493 494 # Calculate ATR 495 tr1 = high - low 496 tr2 = abs(high - close.shift(1)) 497 tr3 = abs(low - close.shift(1)) 498 tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) 499 atr = tr.ewm(alpha=1 / atr_period, adjust=False).mean() 500 501 # Calculate basic bands 502 hl2 = (high + low) / 2 503 basic_upper = hl2 + (multiplier * atr) 504 basic_lower = hl2 - (multiplier * atr) 505 506 # Initialize arrays 507 n = len(df) 508 supertrend = np.zeros(n) 509 direction = np.zeros(n) 510 final_upper = np.zeros(n) 511 final_lower = np.zeros(n) 512 513 # First value 514 final_upper[0] = basic_upper.iloc[0] 515 final_lower[0] = basic_lower.iloc[0] 516 supertrend[0] = basic_upper.iloc[0] 517 direction[0] = -1 # Start bearish 518 519 # Calculate Supertrend 520 for i in range(1, n): 521 # Final Upper Band 522 if basic_upper.iloc[i] < final_upper[i - 1] or close.iloc[i - 1] > final_upper[i - 1]: 523 final_upper[i] = basic_upper.iloc[i] 524 else: 525 final_upper[i] = final_upper[i - 1] 526 527 # Final Lower Band 528 if basic_lower.iloc[i] > final_lower[i - 1] or close.iloc[i - 1] < final_lower[i - 1]: 529 final_lower[i] = basic_lower.iloc[i] 530 else: 531 final_lower[i] = final_lower[i - 1] 532 533 # Supertrend and Direction 534 if supertrend[i - 1] == final_upper[i - 1]: 535 # Was bearish 536 if close.iloc[i] > final_upper[i]: 537 supertrend[i] = final_lower[i] 538 direction[i] = 1 # Bullish 539 else: 540 supertrend[i] = final_upper[i] 541 direction[i] = -1 # Bearish 542 else: 543 # Was bullish 544 if close.iloc[i] < final_lower[i]: 545 supertrend[i] = final_upper[i] 546 direction[i] = -1 # Bearish 547 else: 548 supertrend[i] = final_lower[i] 549 direction[i] = 1 # Bullish 550 551 return pd.DataFrame( 552 { 553 "Supertrend": supertrend, 554 "Supertrend_Direction": direction, 555 "Supertrend_Upper": final_upper, 556 "Supertrend_Lower": final_lower, 557 }, 558 index=df.index, 559 )
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
100def calculate_tilson_t3( 101 df: pd.DataFrame, 102 period: int = 5, 103 vfactor: float = 0.7, 104 column: str = "Close", 105) -> pd.Series: 106 """Calculate Tilson T3 Moving Average. 107 108 T3 is a triple-smoothed exponential moving average that reduces lag 109 while maintaining smoothness. Developed by Tim Tilson. 110 111 The T3 uses a volume factor (vfactor) to control the amount of 112 smoothing vs responsiveness: 113 - vfactor = 0: T3 behaves like a triple EMA 114 - vfactor = 1: Maximum smoothing (may overshoot) 115 - vfactor = 0.7: Tilson's recommended default 116 117 Args: 118 df: DataFrame with price data 119 period: Number of periods for EMA calculations (default 5) 120 vfactor: Volume factor for smoothing (0-1, default 0.7) 121 column: Column name to use for calculation 122 123 Returns: 124 Series with T3 values 125 126 Examples: 127 >>> t3 = calculate_tilson_t3(df, period=5, vfactor=0.7) 128 >>> # More responsive (less smooth) 129 >>> t3_fast = calculate_tilson_t3(df, period=5, vfactor=0.5) 130 >>> # More smooth (more lag) 131 >>> t3_smooth = calculate_tilson_t3(df, period=5, vfactor=0.9) 132 """ 133 col = _get_price_column(df, column) 134 if col not in df.columns: 135 return pd.Series(np.nan, index=df.index, name=f"T3_{period}") 136 137 # Calculate coefficients 138 c1 = -(vfactor**3) 139 c2 = 3 * vfactor**2 + 3 * vfactor**3 140 c3 = -6 * vfactor**2 - 3 * vfactor - 3 * vfactor**3 141 c4 = 1 + 3 * vfactor + vfactor**3 + 3 * vfactor**2 142 143 # Calculate 6 consecutive EMAs 144 ema1 = df[col].ewm(span=period, adjust=False).mean() 145 ema2 = ema1.ewm(span=period, adjust=False).mean() 146 ema3 = ema2.ewm(span=period, adjust=False).mean() 147 ema4 = ema3.ewm(span=period, adjust=False).mean() 148 ema5 = ema4.ewm(span=period, adjust=False).mean() 149 ema6 = ema5.ewm(span=period, adjust=False).mean() 150 151 # T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3 152 t3 = c1 * ema6 + c2 * ema5 + c3 * ema4 + c4 * ema3 153 154 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)
567def calculate_hhv( 568 df: pd.DataFrame, period: int = 14, column: str = "High" 569) -> pd.Series: 570 """Calculate Highest High Value (HHV) - MetaStock indicator. 571 572 Returns the highest value of a column over a rolling window. 573 574 Args: 575 df: DataFrame with price data 576 period: Lookback period (default 14) 577 column: Column to use (default "High") 578 579 Returns: 580 Series with HHV values 581 """ 582 col = _get_price_column(df, column) 583 if col not in df.columns: 584 return pd.Series(np.nan, index=df.index, name=f"HHV_{period}") 585 return df[col].rolling(window=period, min_periods=1).max()
Calculate Highest High Value (HHV) - MetaStock indicator.
Returns the highest value of a column over a rolling window.
Args: df: DataFrame with price data period: Lookback period (default 14) column: Column to use (default "High")
Returns: Series with HHV values
588def calculate_llv( 589 df: pd.DataFrame, period: int = 14, column: str = "Low" 590) -> pd.Series: 591 """Calculate Lowest Low Value (LLV) - MetaStock indicator. 592 593 Returns the lowest value of a column over a rolling window. 594 595 Args: 596 df: DataFrame with price data 597 period: Lookback period (default 14) 598 column: Column to use (default "Low") 599 600 Returns: 601 Series with LLV values 602 """ 603 col = _get_price_column(df, column) 604 if col not in df.columns: 605 return pd.Series(np.nan, index=df.index, name=f"LLV_{period}") 606 return df[col].rolling(window=period, min_periods=1).min()
Calculate Lowest Low Value (LLV) - MetaStock indicator.
Returns the lowest value of a column over a rolling window.
Args: df: DataFrame with price data period: Lookback period (default 14) column: Column to use (default "Low")
Returns: Series with LLV values
609def calculate_mom( 610 df: pd.DataFrame, period: int = 10, column: str = "Close" 611) -> pd.Series: 612 """Calculate Momentum (MOM) - MetaStock indicator. 613 614 MOM = Close - Close[N periods ago] 615 616 Args: 617 df: DataFrame with price data 618 period: Lookback period (default 10) 619 column: Column to use (default "Close") 620 621 Returns: 622 Series with Momentum values 623 """ 624 col = _get_price_column(df, column) 625 if col not in df.columns: 626 return pd.Series(np.nan, index=df.index, name=f"MOM_{period}") 627 return df[col] - df[col].shift(period)
Calculate Momentum (MOM) - MetaStock indicator.
MOM = Close - Close[N periods ago]
Args: df: DataFrame with price data period: Lookback period (default 10) column: Column to use (default "Close")
Returns: Series with Momentum values
630def calculate_roc( 631 df: pd.DataFrame, period: int = 10, column: str = "Close" 632) -> pd.Series: 633 """Calculate Rate of Change (ROC) - MetaStock indicator. 634 635 ROC = ((Close - Close[N]) / Close[N]) * 100 636 637 Args: 638 df: DataFrame with price data 639 period: Lookback period (default 10) 640 column: Column to use (default "Close") 641 642 Returns: 643 Series with ROC values (percentage) 644 """ 645 col = _get_price_column(df, column) 646 if col not in df.columns: 647 return pd.Series(np.nan, index=df.index, name=f"ROC_{period}") 648 shifted = df[col].shift(period) 649 return ((df[col] - shifted) / shifted) * 100
Calculate Rate of Change (ROC) - MetaStock indicator.
ROC = ((Close - Close[N]) / Close[N]) * 100
Args: df: DataFrame with price data period: Lookback period (default 10) column: Column to use (default "Close")
Returns: Series with ROC values (percentage)
652def calculate_wma( 653 df: pd.DataFrame, period: int = 20, column: str = "Close" 654) -> pd.Series: 655 """Calculate Weighted Moving Average (WMA) - MetaStock indicator. 656 657 WMA assigns linearly increasing weights to recent data. 658 Weight for period i = i (most recent gets highest weight). 659 660 Args: 661 df: DataFrame with price data 662 period: Number of periods (default 20) 663 column: Column to use (default "Close") 664 665 Returns: 666 Series with WMA values 667 """ 668 col = _get_price_column(df, column) 669 if col not in df.columns: 670 return pd.Series(np.nan, index=df.index, name=f"WMA_{period}") 671 weights = np.arange(1, period + 1, dtype=float) 672 return df[col].rolling(window=period, min_periods=period).apply( 673 lambda x: np.dot(x, weights) / weights.sum(), raw=True 674 )
Calculate Weighted Moving Average (WMA) - MetaStock indicator.
WMA assigns linearly increasing weights to recent data. Weight for period i = i (most recent gets highest weight).
Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")
Returns: Series with WMA values
677def calculate_dema( 678 df: pd.DataFrame, period: int = 20, column: str = "Close" 679) -> pd.Series: 680 """Calculate Double Exponential Moving Average (DEMA) - MetaStock indicator. 681 682 DEMA = 2 * EMA(Close, N) - EMA(EMA(Close, N), N) 683 684 Args: 685 df: DataFrame with price data 686 period: Number of periods (default 20) 687 column: Column to use (default "Close") 688 689 Returns: 690 Series with DEMA values 691 """ 692 col = _get_price_column(df, column) 693 if col not in df.columns: 694 return pd.Series(np.nan, index=df.index, name=f"DEMA_{period}") 695 ema1 = df[col].ewm(span=period, adjust=False).mean() 696 ema2 = ema1.ewm(span=period, adjust=False).mean() 697 return 2 * ema1 - ema2
Calculate Double Exponential Moving Average (DEMA) - MetaStock indicator.
DEMA = 2 * EMA(Close, N) - EMA(EMA(Close, N), N)
Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")
Returns: Series with DEMA values
700def calculate_tema( 701 df: pd.DataFrame, period: int = 20, column: str = "Close" 702) -> pd.Series: 703 """Calculate Triple Exponential Moving Average (TEMA) - MetaStock indicator. 704 705 TEMA = 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA)) 706 707 Args: 708 df: DataFrame with price data 709 period: Number of periods (default 20) 710 column: Column to use (default "Close") 711 712 Returns: 713 Series with TEMA values 714 """ 715 col = _get_price_column(df, column) 716 if col not in df.columns: 717 return pd.Series(np.nan, index=df.index, name=f"TEMA_{period}") 718 ema1 = df[col].ewm(span=period, adjust=False).mean() 719 ema2 = ema1.ewm(span=period, adjust=False).mean() 720 ema3 = ema2.ewm(span=period, adjust=False).mean() 721 return 3 * ema1 - 3 * ema2 + ema3
Calculate Triple Exponential Moving Average (TEMA) - MetaStock indicator.
TEMA = 3EMA - 3EMA(EMA) + EMA(EMA(EMA))
Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")
Returns: Series with TEMA values
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}")
248def withholding_tax_rate( 249 fund_code: str, 250 purchase_date: date | str | None = None, 251 holding_days: int | None = None, 252) -> float | None: 253 """Get the withholding tax rate for a specific fund. 254 255 Convenience wrapper that fetches fund info from TEFAS, classifies the 256 fund's tax category, and returns the applicable rate. 257 258 Args: 259 fund_code: TEFAS fund code (e.g., "AAK", "TTE"). 260 purchase_date: Date of fund purchase. Defaults to today. 261 holding_days: Number of days held (relevant for GSYF/GYF funds). 262 263 Returns: 264 Tax rate as a decimal (e.g., 0.15 for 15%), or None if fund 265 category cannot be determined. 266 267 Examples: 268 >>> import borsapy as bp 269 >>> bp.withholding_tax_rate("AAK", "2025-06-01") 270 0.15 271 >>> bp.withholding_tax_rate("AAK", "2025-08-01") 272 0.175 273 """ 274 from borsapy.fund import Fund 275 276 if purchase_date is None: 277 purchase_date = date.today() 278 279 fund = Fund(fund_code) 280 info = fund.info 281 category = info.get("category", "") or "" 282 fund_name = info.get("name", "") or "" 283 284 tax_cat = classify_fund_tax_category(category, fund_name) 285 if tax_cat is None: 286 return None 287 288 return get_withholding_tax_rate(tax_cat, purchase_date, holding_days)
Get the withholding tax rate for a specific fund.
Convenience wrapper that fetches fund info from TEFAS, classifies the fund's tax category, and returns the applicable rate.
Args: fund_code: TEFAS fund code (e.g., "AAK", "TTE"). purchase_date: Date of fund purchase. Defaults to today. holding_days: Number of days held (relevant for GSYF/GYF funds).
Returns: Tax rate as a decimal (e.g., 0.15 for 15%), or None if fund category cannot be determined.
Examples:
import borsapy as bp bp.withholding_tax_rate("AAK", "2025-06-01") 0.15 bp.withholding_tax_rate("AAK", "2025-08-01") 0.175
291def withholding_tax_table() -> pd.DataFrame: 292 """Return the full withholding tax reference table. 293 294 Returns: 295 DataFrame with columns: tax_category, description, and one column 296 per date period showing the tax rate as a percentage. 297 298 Examples: 299 >>> import borsapy as bp 300 >>> bp.withholding_tax_table() 301 tax_category description <23.12.2020 ... >=09.07.2025 302 0 degisken_karma_doviz Degisken, karma, ... 10.0 ... 17.5 303 1 pay_senedi_yogun Pay senedi yogun fon 0.0 ... 0.0 304 ... 305 """ 306 period_labels = [ 307 "<23.12.2020", 308 "23.12.2020-30.04.2024", 309 "01.05.2024-31.10.2024", 310 "01.11.2024-31.01.2025", 311 "01.02.2025-08.07.2025", 312 ">=09.07.2025", 313 ] 314 315 rows = [] 316 for cat in [ 317 TAX_CAT_VARIABLE, 318 TAX_CAT_EQUITY_HEAVY, 319 TAX_CAT_OTHER, 320 TAX_CAT_GSYF_GYF_LONG, 321 TAX_CAT_GSYF_GYF_SHORT, 322 ]: 323 row = { 324 "tax_category": cat, 325 "description": TAX_CAT_DESCRIPTIONS[cat], 326 } 327 for label, rate in zip(period_labels, TAX_RATES[cat], strict=True): 328 row[label] = rate 329 rows.append(row) 330 331 return pd.DataFrame(rows)
Return the full withholding tax reference table.
Returns: DataFrame with columns: tax_category, description, and one column per date period showing the tax rate as a percentage.
Examples:
import borsapy as bp bp.withholding_tax_table() tax_category description <23.12.2020 ... >=09.07.2025 0 degisken_karma_doviz Degisken, karma, ... 10.0 ... 17.5 1 pay_senedi_yogun Pay senedi yogun fon 0.0 ... 0.0 ...
20def set_twitter_auth( 21 auth_token: str | None = None, 22 ct0: str | None = None, 23 cookies: dict | None = None, 24 cookies_file: str | None = None, 25) -> None: 26 """ 27 Set Twitter/X authentication credentials for tweet search. 28 29 Twitter requires cookie-based authentication. You can get these values 30 from your browser's developer tools after logging into twitter.com/x.com. 31 32 Args: 33 auth_token: The auth_token cookie value from Twitter/X. 34 ct0: The ct0 cookie value from Twitter/X. 35 cookies: Dict with 'auth_token' and 'ct0' keys. 36 cookies_file: Path to a cookies JSON file (Scweet format). 37 38 Examples: 39 >>> import borsapy as bp 40 >>> # Method 1: Direct cookie values 41 >>> bp.set_twitter_auth(auth_token="abc123...", ct0="xyz789...") 42 >>> # Method 2: Dict 43 >>> bp.set_twitter_auth(cookies={"auth_token": "abc123", "ct0": "xyz789"}) 44 >>> # Method 3: Cookies file 45 >>> bp.set_twitter_auth(cookies_file="cookies.json") 46 """ 47 global _twitter_credentials 48 49 if cookies_file: 50 _twitter_credentials = {"cookies_file": cookies_file} 51 elif cookies: 52 if "auth_token" not in cookies: 53 raise ValueError("cookies dict must contain 'auth_token' key") 54 _twitter_credentials = {"cookies": cookies} 55 elif auth_token: 56 _twitter_credentials = {"cookies": {"auth_token": auth_token, "ct0": ct0 or ""}} 57 else: 58 raise ValueError( 59 "Provide auth_token/ct0, cookies dict, or cookies_file. " 60 "Get auth_token and ct0 from browser DevTools > Application > Cookies > x.com" 61 )
Set Twitter/X authentication credentials for tweet search.
Twitter requires cookie-based authentication. You can get these values from your browser's developer tools after logging into twitter.com/x.com.
Args: auth_token: The auth_token cookie value from Twitter/X. ct0: The ct0 cookie value from Twitter/X. cookies: Dict with 'auth_token' and 'ct0' keys. cookies_file: Path to a cookies JSON file (Scweet format).
Examples:
import borsapy as bp
Method 1: Direct cookie values
bp.set_twitter_auth(auth_token="abc123...", ct0="xyz789...")
Method 2: Dict
bp.set_twitter_auth(cookies={"auth_token": "abc123", "ct0": "xyz789"})
Method 3: Cookies file
bp.set_twitter_auth(cookies_file="cookies.json")
70def get_twitter_auth() -> dict | None: 71 """Get current Twitter/X authentication credentials.""" 72 return _twitter_credentials
Get current Twitter/X authentication credentials.
64def clear_twitter_auth() -> None: 65 """Clear Twitter/X authentication credentials.""" 66 global _twitter_credentials 67 _twitter_credentials = None
Clear Twitter/X authentication credentials.
150def search_tweets( 151 query: str, 152 period: str | None = "7d", 153 since: str | None = None, 154 until: str | None = None, 155 lang: str | None = None, 156 limit: int = 100, 157) -> pd.DataFrame: 158 """ 159 Search Twitter/X for tweets matching a query. 160 161 Requires authentication: call bp.set_twitter_auth() first. 162 Requires optional dependency: pip install borsapy[twitter] 163 164 Args: 165 query: Search query (e.g., "$THYAO", "dolar kur", "#Bitcoin"). 166 period: Time period ("1d", "7d", "1mo"). Ignored if since/until set. 167 since: Start date (YYYY-MM-DD). Overrides period. 168 until: End date (YYYY-MM-DD). Overrides period. 169 lang: Language filter (e.g., "tr", "en"). 170 limit: Maximum number of tweets (default 100). 171 172 Returns: 173 DataFrame with columns: tweet_id, created_at, text, author_handle, 174 author_name, likes, retweets, replies, views, quotes, bookmarks, 175 author_followers, author_verified, lang, url. 176 177 Examples: 178 >>> import borsapy as bp 179 >>> bp.set_twitter_auth(auth_token="...", ct0="...") 180 >>> df = bp.search_tweets("$THYAO", period="7d") 181 >>> df = bp.search_tweets("dolar kur", period="1d", lang="tr", limit=50) 182 """ 183 provider = get_twitter_provider() 184 return provider.search_tweets( 185 query=query, 186 period=period, 187 since=since, 188 until=until, 189 lang=lang, 190 limit=limit, 191 )
Search Twitter/X for tweets matching a query.
Requires authentication: call bp.set_twitter_auth() first. Requires optional dependency: pip install borsapy[twitter]
Args: query: Search query (e.g., "$THYAO", "dolar kur", "#Bitcoin"). period: Time period ("1d", "7d", "1mo"). Ignored if since/until set. since: Start date (YYYY-MM-DD). Overrides period. until: End date (YYYY-MM-DD). Overrides period. lang: Language filter (e.g., "tr", "en"). limit: Maximum number of tweets (default 100).
Returns: DataFrame with columns: tweet_id, created_at, text, author_handle, author_name, likes, retweets, replies, views, quotes, bookmarks, author_followers, author_verified, lang, url.
Examples:
import borsapy as bp bp.set_twitter_auth(auth_token="...", ct0="...") df = bp.search_tweets("$THYAO", period="7d") df = bp.search_tweets("dolar kur", period="1d", lang="tr", limit=50)