borsapy

borsapy - Turkish Financial Markets Data Library

A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data.

Examples:

import borsapy as bp

# Get stock data
>>> stock = bp.Ticker("THYAO")
>>> stock.info  # Real-time quote
>>> stock.history(period="1mo")  # OHLCV data
>>> stock.balance_sheet  # Financial statements

# Get forex/commodity data
>>> usd = bp.FX("USD")
>>> usd.current  # Current rate
>>> usd.history(period="1mo")  # Historical data
>>> usd.bank_rates  # Bank exchange rates
>>> usd.bank_rate("akbank")  # Single bank rate
>>> bp.banks()  # List supported banks
>>> gold = bp.FX("gram-altin")

# List all BIST companies
>>> bp.companies()
>>> bp.search_companies("banka")

# Get crypto data
>>> btc = bp.Crypto("BTCTRY")
>>> btc.current  # Current price
>>> btc.history(period="1mo")  # Historical OHLCV
>>> bp.crypto_pairs()  # List available pairs

# Get fund data
>>> fund = bp.Fund("AAK")
>>> fund.info  # Fund details
>>> fund.history(period="1mo")  # Price history

# Get inflation data
>>> inf = bp.Inflation()
>>> inf.latest()  # Latest TÜFE data
>>> inf.calculate(100000, "2020-01", "2024-01")  # Inflation calculation

# Economic calendar
>>> cal = bp.EconomicCalendar()
>>> cal.events(period="1w")  # This week's events
>>> cal.today()  # Today's events
>>> bp.economic_calendar(country="TR", importance="high")

# Government bonds
>>> bp.bonds()  # All bond yields
>>> bond = bp.Bond("10Y")
>>> bond.yield_rate  # Current 10Y yield
>>> bp.risk_free_rate()  # For DCF calculations

# Stock screener
>>> bp.screen_stocks(template="high_dividend")
>>> bp.screen_stocks(market_cap_min=1000, pe_max=15)
>>> screener = bp.Screener()
>>> screener.add_filter("dividend_yield", min=3).run()

# Real-time streaming (low-latency)
>>> stream = bp.TradingViewStream()
>>> stream.connect()
>>> stream.subscribe("THYAO")
>>> quote = stream.get_quote("THYAO")  # Instant (~1ms)
>>> quote['last']  # Last price
>>> stream.disconnect()

# Context manager
>>> with bp.TradingViewStream() as stream:
...     stream.subscribe("THYAO")
...     quote = stream.wait_for_quote("THYAO")
...     print(quote['last'])

# Symbol search
>>> bp.search("banka")  # Search all markets
>>> bp.search_bist("enerji")  # BIST only
>>> bp.search_crypto("BTC")  # Crypto only
>>> bp.search("THYAO", full_info=True)  # Detailed results

# Heikin Ashi charts
>>> df = stock.history(period="1y")
>>> ha_df = bp.calculate_heikin_ashi(df)  # Returns HA_Open, HA_High, HA_Low, HA_Close
>>> ha_df = stock.heikin_ashi(period="1y")  # Convenience method

# Chart streaming (OHLCV candles via WebSocket)
>>> stream = bp.TradingViewStream()
>>> stream.connect()
>>> stream.subscribe_chart("THYAO", "1m")  # 1-minute candles
>>> candle = stream.get_candle("THYAO", "1m")
>>> print(candle['close'])

# Historical replay for backtesting
>>> session = bp.create_replay("THYAO", period="1y", speed=100)
>>> for candle in session.replay():
...     print(f"{candle['timestamp']}: {candle['close']}")

# Backtest engine
>>> def rsi_strategy(candle, position, indicators):
...     if indicators['rsi'] < 30 and position is None:
...         return 'BUY'
...     elif indicators['rsi'] > 70 and position == 'long':
...         return 'SELL'
...     return 'HOLD'
>>> result = bp.backtest("THYAO", rsi_strategy, period="1y", indicators=['rsi'])
>>> print(result.summary())
>>> print(f"Win Rate: {result.win_rate:.1f}%")

# Pine Script streaming indicators
>>> stream = bp.TradingViewStream()
>>> stream.connect()
>>> stream.subscribe_chart("THYAO", "1m")
>>> stream.add_study("THYAO", "1m", "RSI")
>>> stream.add_study("THYAO", "1m", "MACD")
>>> rsi = stream.get_study("THYAO", "1m", "RSI")
>>> print(rsi['value'])
  1"""
  2borsapy - Turkish Financial Markets Data Library
  3
  4A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data.
  5
  6Examples:
  7    >>> import borsapy as bp
  8
  9    # Get stock data
 10    >>> stock = bp.Ticker("THYAO")
 11    >>> stock.info  # Real-time quote
 12    >>> stock.history(period="1mo")  # OHLCV data
 13    >>> stock.balance_sheet  # Financial statements
 14
 15    # Get forex/commodity data
 16    >>> usd = bp.FX("USD")
 17    >>> usd.current  # Current rate
 18    >>> usd.history(period="1mo")  # Historical data
 19    >>> usd.bank_rates  # Bank exchange rates
 20    >>> usd.bank_rate("akbank")  # Single bank rate
 21    >>> bp.banks()  # List supported banks
 22    >>> gold = bp.FX("gram-altin")
 23
 24    # List all BIST companies
 25    >>> bp.companies()
 26    >>> bp.search_companies("banka")
 27
 28    # Get crypto data
 29    >>> btc = bp.Crypto("BTCTRY")
 30    >>> btc.current  # Current price
 31    >>> btc.history(period="1mo")  # Historical OHLCV
 32    >>> bp.crypto_pairs()  # List available pairs
 33
 34    # Get fund data
 35    >>> fund = bp.Fund("AAK")
 36    >>> fund.info  # Fund details
 37    >>> fund.history(period="1mo")  # Price history
 38
 39    # Get inflation data
 40    >>> inf = bp.Inflation()
 41    >>> inf.latest()  # Latest TÜFE data
 42    >>> inf.calculate(100000, "2020-01", "2024-01")  # Inflation calculation
 43
 44    # Economic calendar
 45    >>> cal = bp.EconomicCalendar()
 46    >>> cal.events(period="1w")  # This week's events
 47    >>> cal.today()  # Today's events
 48    >>> bp.economic_calendar(country="TR", importance="high")
 49
 50    # Government bonds
 51    >>> bp.bonds()  # All bond yields
 52    >>> bond = bp.Bond("10Y")
 53    >>> bond.yield_rate  # Current 10Y yield
 54    >>> bp.risk_free_rate()  # For DCF calculations
 55
 56    # Stock screener
 57    >>> bp.screen_stocks(template="high_dividend")
 58    >>> bp.screen_stocks(market_cap_min=1000, pe_max=15)
 59    >>> screener = bp.Screener()
 60    >>> screener.add_filter("dividend_yield", min=3).run()
 61
 62    # Real-time streaming (low-latency)
 63    >>> stream = bp.TradingViewStream()
 64    >>> stream.connect()
 65    >>> stream.subscribe("THYAO")
 66    >>> quote = stream.get_quote("THYAO")  # Instant (~1ms)
 67    >>> quote['last']  # Last price
 68    >>> stream.disconnect()
 69
 70    # Context manager
 71    >>> with bp.TradingViewStream() as stream:
 72    ...     stream.subscribe("THYAO")
 73    ...     quote = stream.wait_for_quote("THYAO")
 74    ...     print(quote['last'])
 75
 76    # Symbol search
 77    >>> bp.search("banka")  # Search all markets
 78    >>> bp.search_bist("enerji")  # BIST only
 79    >>> bp.search_crypto("BTC")  # Crypto only
 80    >>> bp.search("THYAO", full_info=True)  # Detailed results
 81
 82    # Heikin Ashi charts
 83    >>> df = stock.history(period="1y")
 84    >>> ha_df = bp.calculate_heikin_ashi(df)  # Returns HA_Open, HA_High, HA_Low, HA_Close
 85    >>> ha_df = stock.heikin_ashi(period="1y")  # Convenience method
 86
 87    # Chart streaming (OHLCV candles via WebSocket)
 88    >>> stream = bp.TradingViewStream()
 89    >>> stream.connect()
 90    >>> stream.subscribe_chart("THYAO", "1m")  # 1-minute candles
 91    >>> candle = stream.get_candle("THYAO", "1m")
 92    >>> print(candle['close'])
 93
 94    # Historical replay for backtesting
 95    >>> session = bp.create_replay("THYAO", period="1y", speed=100)
 96    >>> for candle in session.replay():
 97    ...     print(f"{candle['timestamp']}: {candle['close']}")
 98
 99    # Backtest engine
100    >>> def rsi_strategy(candle, position, indicators):
101    ...     if indicators['rsi'] < 30 and position is None:
102    ...         return 'BUY'
103    ...     elif indicators['rsi'] > 70 and position == 'long':
104    ...         return 'SELL'
105    ...     return 'HOLD'
106    >>> result = bp.backtest("THYAO", rsi_strategy, period="1y", indicators=['rsi'])
107    >>> print(result.summary())
108    >>> print(f"Win Rate: {result.win_rate:.1f}%")
109
110    # Pine Script streaming indicators
111    >>> stream = bp.TradingViewStream()
112    >>> stream.connect()
113    >>> stream.subscribe_chart("THYAO", "1m")
114    >>> stream.add_study("THYAO", "1m", "RSI")
115    >>> stream.add_study("THYAO", "1m", "MACD")
116    >>> rsi = stream.get_study("THYAO", "1m", "RSI")
117    >>> print(rsi['value'])
118"""
119
120# TradingView authentication for real-time data
121from borsapy._providers.tradingview import (
122    clear_tradingview_auth,
123    get_tradingview_auth,
124    set_tradingview_auth,
125)
126from borsapy.backtest import Backtest, BacktestResult, Trade, backtest
127from borsapy.bond import Bond, bonds, risk_free_rate
128from borsapy.calendar import EconomicCalendar, economic_calendar
129from borsapy.charts import calculate_heikin_ashi
130from borsapy.crypto import Crypto, crypto_pairs
131from borsapy.eurobond import Eurobond, eurobonds
132from borsapy.exceptions import (
133    APIError,
134    AuthenticationError,
135    BorsapyError,
136    DataNotAvailableError,
137    InvalidIntervalError,
138    InvalidPeriodError,
139    RateLimitError,
140    TickerNotFoundError,
141)
142from borsapy.fund import Fund, compare_funds, screen_funds, search_funds
143from borsapy.fx import FX, banks, metal_institutions
144from borsapy.index import Index, all_indices, index, indices
145from borsapy.inflation import Inflation
146from borsapy.market import companies, search_companies
147from borsapy.multi import Tickers, download
148from borsapy.portfolio import Portfolio
149from borsapy.replay import ReplaySession, create_replay
150from borsapy.scanner import ScanResult, TechnicalScanner, scan
151from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices
152from borsapy.search import (
153    search,
154    search_bist,
155    search_crypto,
156    search_forex,
157    search_index,
158    search_viop,
159    viop_contracts,
160)
161
162# TradingView streaming for real-time updates
163from borsapy.stream import TradingViewStream, create_stream
164from borsapy.tcmb import TCMB, policy_rate
165from borsapy.technical import (
166    TechnicalAnalyzer,
167    add_indicators,
168    calculate_adx,
169    calculate_atr,
170    calculate_bollinger_bands,
171    calculate_ema,
172    calculate_macd,
173    calculate_obv,
174    calculate_rsi,
175    calculate_sma,
176    calculate_stochastic,
177    calculate_supertrend,
178    calculate_tilson_t3,
179    calculate_vwap,
180)
181from borsapy.ticker import Ticker
182from borsapy.viop import VIOP
183
184__version__ = "0.7.2"
185__author__ = "Said Surucu"
186
187__all__ = [
188    # Main classes
189    "Ticker",
190    "Tickers",
191    "FX",
192    "Crypto",
193    "Fund",
194    "Portfolio",
195    "Index",
196    "Inflation",
197    "VIOP",
198    "Bond",
199    "Eurobond",
200    "TCMB",
201    "EconomicCalendar",
202    "Screener",
203    "TradingViewStream",
204    "ReplaySession",
205    # Market functions
206    "companies",
207    "search_companies",
208    "search",
209    "search_bist",
210    "search_crypto",
211    "search_forex",
212    "search_index",
213    "search_viop",
214    "viop_contracts",
215    "banks",
216    "metal_institutions",
217    "crypto_pairs",
218    "search_funds",
219    "screen_funds",
220    "compare_funds",
221    "download",
222    "index",
223    "indices",
224    "all_indices",
225    # Bond functions
226    "bonds",
227    "risk_free_rate",
228    # Eurobond functions
229    "eurobonds",
230    # TCMB functions
231    "policy_rate",
232    # Calendar functions
233    "economic_calendar",
234    # Screener functions
235    "screen_stocks",
236    "screener_criteria",
237    "sectors",
238    "stock_indices",
239    # Technical Scanner
240    "TechnicalScanner",
241    "ScanResult",
242    "scan",
243    # Technical analysis
244    "TechnicalAnalyzer",
245    "add_indicators",
246    "calculate_sma",
247    "calculate_ema",
248    "calculate_rsi",
249    "calculate_macd",
250    "calculate_bollinger_bands",
251    "calculate_atr",
252    "calculate_stochastic",
253    "calculate_obv",
254    "calculate_vwap",
255    "calculate_adx",
256    "calculate_supertrend",
257    "calculate_tilson_t3",
258    # Charts
259    "calculate_heikin_ashi",
260    # Replay
261    "ReplaySession",
262    "create_replay",
263    # Exceptions
264    "BorsapyError",
265    "TickerNotFoundError",
266    "DataNotAvailableError",
267    "APIError",
268    "AuthenticationError",
269    "RateLimitError",
270    "InvalidPeriodError",
271    "InvalidIntervalError",
272    # TradingView authentication (premium)
273    "set_tradingview_auth",
274    "get_tradingview_auth",
275    "clear_tradingview_auth",
276    # TradingView streaming (real-time)
277    "TradingViewStream",
278    "create_stream",
279    # Backtest engine
280    "Backtest",
281    "BacktestResult",
282    "Trade",
283    "backtest",
284]
class Ticker(borsapy.technical.TechnicalMixin):
 468class Ticker(TechnicalMixin):
 469    """
 470    A yfinance-like interface for Turkish stock data.
 471
 472    Examples:
 473        >>> import borsapy as bp
 474        >>> stock = bp.Ticker("THYAO")
 475        >>> stock.info
 476        {'symbol': 'THYAO', 'last': 268.5, ...}
 477        >>> stock.history(period="1mo")
 478                         Open    High     Low   Close    Volume
 479        Date
 480        2024-12-01    265.00  268.00  264.00  267.50  12345678
 481        ...
 482    """
 483
 484    def __init__(self, symbol: str):
 485        """
 486        Initialize a Ticker object.
 487
 488        Args:
 489            symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS").
 490                    The ".IS" or ".E" suffix is optional and will be removed.
 491        """
 492        self._symbol = symbol.upper().replace(".IS", "").replace(".E", "")
 493        self._tradingview = get_tradingview_provider()
 494        self._isyatirim = None  # Lazy load for financial statements
 495        self._kap = None  # Lazy load for KAP disclosures
 496        self._isin_provider = None  # Lazy load for ISIN lookup
 497        self._hedeffiyat = None  # Lazy load for analyst price targets
 498        self._etf_provider = None  # Lazy load for ETF holders
 499
 500    def _get_isyatirim(self):
 501        """Lazy load İş Yatırım provider for financial statements."""
 502        if self._isyatirim is None:
 503            from borsapy._providers.isyatirim import get_isyatirim_provider
 504
 505            self._isyatirim = get_isyatirim_provider()
 506        return self._isyatirim
 507
 508    def _get_kap(self):
 509        """Lazy load KAP provider for disclosures and calendar."""
 510        if self._kap is None:
 511            from borsapy._providers.kap import get_kap_provider
 512
 513            self._kap = get_kap_provider()
 514        return self._kap
 515
 516    def _get_isin_provider(self):
 517        """Lazy load ISIN provider."""
 518        if self._isin_provider is None:
 519            from borsapy._providers.isin import get_isin_provider
 520
 521            self._isin_provider = get_isin_provider()
 522        return self._isin_provider
 523
 524    def _get_hedeffiyat(self):
 525        """Lazy load hedeffiyat.com.tr provider for analyst price targets."""
 526        if self._hedeffiyat is None:
 527            from borsapy._providers.hedeffiyat import get_hedeffiyat_provider
 528
 529            self._hedeffiyat = get_hedeffiyat_provider()
 530        return self._hedeffiyat
 531
 532    def _get_etf_provider(self):
 533        """Lazy load TradingView ETF provider for ETF holders."""
 534        if self._etf_provider is None:
 535            from borsapy._providers.tradingview_etf import get_tradingview_etf_provider
 536
 537            self._etf_provider = get_tradingview_etf_provider()
 538        return self._etf_provider
 539
 540    @property
 541    def symbol(self) -> str:
 542        """Return the ticker symbol."""
 543        return self._symbol
 544
 545    @property
 546    def fast_info(self) -> FastInfo:
 547        """
 548        Get fast access to common ticker information.
 549
 550        Returns a FastInfo object with quick access to frequently used data:
 551        - currency, exchange, timezone
 552        - last_price, open, day_high, day_low, previous_close, volume
 553        - market_cap, shares, pe_ratio, pb_ratio
 554        - year_high, year_low (52-week)
 555        - fifty_day_average, two_hundred_day_average
 556        - free_float, foreign_ratio
 557
 558        Examples:
 559            >>> stock = Ticker("THYAO")
 560            >>> stock.fast_info.market_cap
 561            370530000000
 562            >>> stock.fast_info['pe_ratio']
 563            2.8
 564            >>> stock.fast_info.keys()
 565            ['currency', 'exchange', 'timezone', ...]
 566        """
 567        if not hasattr(self, "_fast_info"):
 568            self._fast_info = FastInfo(self)
 569        return self._fast_info
 570
 571    @property
 572    def info(self) -> EnrichedInfo:
 573        """
 574        Get comprehensive ticker information with yfinance-compatible fields.
 575
 576        Returns:
 577            EnrichedInfo object providing dict-like access to:
 578
 579            Basic fields (always loaded, fast):
 580            - symbol, last, open, high, low, close, volume
 581            - change, change_percent, update_time
 582
 583            yfinance aliases (map to basic fields):
 584            - regularMarketPrice, currentPrice -> last
 585            - regularMarketOpen -> open
 586            - regularMarketDayHigh -> high
 587            - regularMarketDayLow -> low
 588            - regularMarketPreviousClose -> close
 589            - regularMarketVolume -> volume
 590
 591            Extended fields (lazy-loaded on access):
 592            - marketCap, trailingPE, priceToBook, enterpriseToEbitda
 593            - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
 594            - fiftyDayAverage, twoHundredDayAverage
 595            - floatShares, foreignRatio, netDebt
 596            - currency, exchange, timezone
 597
 598            Dividend fields (lazy-loaded on access):
 599            - dividendYield, exDividendDate
 600            - trailingAnnualDividendRate, trailingAnnualDividendYield
 601
 602        Examples:
 603            >>> stock = Ticker("THYAO")
 604            >>> stock.info['last']  # Basic field - fast
 605            268.5
 606            >>> stock.info['marketCap']  # Extended field - fetches İş Yatırım
 607            370530000000
 608            >>> stock.info['trailingPE']  # yfinance compatible name
 609            2.8
 610            >>> stock.info.get('dividendYield')  # Safe access
 611            1.28
 612            >>> stock.info.todict()  # Get all as regular dict
 613            {...}
 614        """
 615        if not hasattr(self, "_enriched_info"):
 616            self._enriched_info = EnrichedInfo(self)
 617        return self._enriched_info
 618
 619    def history(
 620        self,
 621        period: str = "1mo",
 622        interval: str = "1d",
 623        start: datetime | str | None = None,
 624        end: datetime | str | None = None,
 625        actions: bool = False,
 626    ) -> pd.DataFrame:
 627        """
 628        Get historical OHLCV data.
 629
 630        Args:
 631            period: How much data to fetch. Valid periods:
 632                    1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
 633                    Ignored if start is provided.
 634            interval: Data granularity. Valid intervals:
 635                      1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo.
 636            start: Start date (string or datetime).
 637            end: End date (string or datetime). Defaults to today.
 638            actions: If True, include Dividends and Stock Splits columns.
 639                     Defaults to False.
 640
 641        Returns:
 642            DataFrame with columns: Open, High, Low, Close, Volume.
 643            If actions=True, also includes Dividends and Stock Splits columns.
 644            Index is the Date.
 645
 646        Examples:
 647            >>> stock = Ticker("THYAO")
 648            >>> stock.history(period="1mo")  # Last month
 649            >>> stock.history(period="1y", interval="1wk")  # Weekly for 1 year
 650            >>> stock.history(start="2024-01-01", end="2024-06-30")  # Date range
 651            >>> stock.history(period="1y", actions=True)  # With dividends/splits
 652        """
 653        # Parse dates if strings
 654        start_dt = self._parse_date(start) if start else None
 655        end_dt = self._parse_date(end) if end else None
 656
 657        df = self._tradingview.get_history(
 658            symbol=self._symbol,
 659            period=period,
 660            interval=interval,
 661            start=start_dt,
 662            end=end_dt,
 663        )
 664
 665        if actions and not df.empty:
 666            df = self._add_actions_to_history(df)
 667
 668        return df
 669
 670    def _add_actions_to_history(self, df: pd.DataFrame) -> pd.DataFrame:
 671        """
 672        Add Dividends and Stock Splits columns to historical data.
 673
 674        Args:
 675            df: Historical OHLCV DataFrame.
 676
 677        Returns:
 678            DataFrame with added Dividends and Stock Splits columns.
 679        """
 680        # Initialize columns with zeros
 681        df = df.copy()
 682        df["Dividends"] = 0.0
 683        df["Stock Splits"] = 0.0
 684
 685        # Get dividends
 686        try:
 687            divs = self.dividends
 688            if not divs.empty:
 689                for div_date, row in divs.iterrows():
 690                    # Use date() for timezone-agnostic comparison
 691                    div_date_only = pd.Timestamp(div_date).date()
 692                    for idx in df.index:
 693                        idx_date_only = pd.Timestamp(idx).date()
 694                        if div_date_only == idx_date_only:
 695                            df.loc[idx, "Dividends"] = row.get("Amount", 0)
 696                            break
 697        except Exception:
 698            pass
 699
 700        # Get stock splits (capital increases)
 701        try:
 702            splits = self.splits
 703            if not splits.empty:
 704                for split_date, row in splits.iterrows():
 705                    # Use date() for timezone-agnostic comparison
 706                    split_date_only = pd.Timestamp(split_date).date()
 707                    # Calculate split ratio
 708                    # BonusFromCapital + BonusFromDividend = total bonus percentage
 709                    bonus_pct = row.get("BonusFromCapital", 0) + row.get(
 710                        "BonusFromDividend", 0
 711                    )
 712                    if bonus_pct > 0:
 713                        # Convert percentage to split ratio (e.g., 20% bonus = 1.2 split)
 714                        split_ratio = 1 + (bonus_pct / 100)
 715                        for idx in df.index:
 716                            idx_date_only = pd.Timestamp(idx).date()
 717                            if split_date_only == idx_date_only:
 718                                df.loc[idx, "Stock Splits"] = split_ratio
 719                                break
 720        except Exception:
 721            pass
 722
 723        return df
 724
 725    @cached_property
 726    def dividends(self) -> pd.DataFrame:
 727        """
 728        Get dividend history.
 729
 730        Returns:
 731            DataFrame with dividend history:
 732            - Amount: Dividend per share (TL)
 733            - GrossRate: Gross dividend rate (%)
 734            - NetRate: Net dividend rate (%)
 735            - TotalDividend: Total dividend distributed (TL)
 736
 737        Examples:
 738            >>> stock = Ticker("THYAO")
 739            >>> stock.dividends
 740                           Amount  GrossRate  NetRate  TotalDividend
 741            Date
 742            2025-09-02     3.442    344.20   292.57  4750000000.0
 743            2025-06-16     3.442    344.20   292.57  4750000000.0
 744        """
 745        return self._get_isyatirim().get_dividends(self._symbol)
 746
 747    @cached_property
 748    def splits(self) -> pd.DataFrame:
 749        """
 750        Get capital increase (split) history.
 751
 752        Note: Turkish market uses capital increases instead of traditional splits.
 753        - RightsIssue: Paid capital increase (bedelli)
 754        - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
 755        - BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
 756
 757        Returns:
 758            DataFrame with capital increase history:
 759            - Capital: New capital after increase (TL)
 760            - RightsIssue: Rights issue rate (%)
 761            - BonusFromCapital: Bonus from capital (%)
 762            - BonusFromDividend: Bonus from dividend (%)
 763
 764        Examples:
 765            >>> stock = Ticker("THYAO")
 766            >>> stock.splits
 767                             Capital  RightsIssue  BonusFromCapital  BonusFromDividend
 768            Date
 769            2013-06-26  1380000000.0         0.0             15.00               0.0
 770            2011-07-11  1200000000.0         0.0              0.00              20.0
 771        """
 772        return self._get_isyatirim().get_capital_increases(self._symbol)
 773
 774    @cached_property
 775    def actions(self) -> pd.DataFrame:
 776        """
 777        Get combined dividends and splits history.
 778
 779        Returns:
 780            DataFrame with combined dividend and split actions:
 781            - Dividends: Dividend per share (TL) or 0
 782            - Splits: Combined split ratio (0 if no split)
 783
 784        Examples:
 785            >>> stock = Ticker("THYAO")
 786            >>> stock.actions
 787                         Dividends  Splits
 788            Date
 789            2025-09-02      3.442    0.0
 790            2013-06-26      0.000   15.0
 791        """
 792        dividends = self.dividends
 793        splits = self.splits
 794
 795        # Merge on index (Date)
 796        if dividends.empty and splits.empty:
 797            return pd.DataFrame(columns=["Dividends", "Splits"])
 798
 799        # Extract relevant columns
 800        div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float)
 801        split_series = (
 802            splits["BonusFromCapital"] + splits["BonusFromDividend"]
 803            if not splits.empty
 804            else pd.Series(dtype=float)
 805        )
 806
 807        # Combine into single DataFrame
 808        result = pd.DataFrame({"Dividends": div_series, "Splits": split_series})
 809        result = result.fillna(0)
 810        result = result.sort_index(ascending=False)
 811
 812        return result
 813
 814    def get_balance_sheet(
 815        self, quarterly: bool = False, financial_group: str | None = None
 816    ) -> pd.DataFrame:
 817        """
 818        Get balance sheet data.
 819
 820        Args:
 821            quarterly: If True, return quarterly data. If False, return annual.
 822            financial_group: Financial group code. Use "UFRS" for banks,
 823                           "XI_29" for industrial companies. If None, defaults to XI_29.
 824
 825        Returns:
 826            DataFrame with balance sheet items as rows and periods as columns.
 827
 828        Examples:
 829            >>> stock = bp.Ticker("THYAO")
 830            >>> stock.get_balance_sheet()  # Annual, industrial
 831            >>> stock.get_balance_sheet(quarterly=True)  # Quarterly
 832
 833            >>> bank = bp.Ticker("AKBNK")
 834            >>> bank.get_balance_sheet(financial_group="UFRS")  # Banks need UFRS
 835        """
 836        return self._get_isyatirim().get_financial_statements(
 837            symbol=self._symbol,
 838            statement_type="balance_sheet",
 839            quarterly=quarterly,
 840            financial_group=financial_group,
 841        )
 842
 843    def get_income_stmt(
 844        self, quarterly: bool = False, financial_group: str | None = None
 845    ) -> pd.DataFrame:
 846        """
 847        Get income statement data.
 848
 849        Args:
 850            quarterly: If True, return quarterly data. If False, return annual.
 851            financial_group: Financial group code. Use "UFRS" for banks,
 852                           "XI_29" for industrial companies. If None, defaults to XI_29.
 853
 854        Returns:
 855            DataFrame with income statement items as rows and periods as columns.
 856
 857        Examples:
 858            >>> stock = bp.Ticker("THYAO")
 859            >>> stock.get_income_stmt()  # Annual
 860            >>> stock.get_income_stmt(quarterly=True)  # Quarterly
 861
 862            >>> bank = bp.Ticker("AKBNK")
 863            >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
 864        """
 865        return self._get_isyatirim().get_financial_statements(
 866            symbol=self._symbol,
 867            statement_type="income_stmt",
 868            quarterly=quarterly,
 869            financial_group=financial_group,
 870        )
 871
 872    def get_cashflow(
 873        self, quarterly: bool = False, financial_group: str | None = None
 874    ) -> pd.DataFrame:
 875        """
 876        Get cash flow statement data.
 877
 878        Args:
 879            quarterly: If True, return quarterly data. If False, return annual.
 880            financial_group: Financial group code. Use "UFRS" for banks,
 881                           "XI_29" for industrial companies. If None, defaults to XI_29.
 882
 883        Returns:
 884            DataFrame with cash flow items as rows and periods as columns.
 885
 886        Examples:
 887            >>> stock = bp.Ticker("THYAO")
 888            >>> stock.get_cashflow()  # Annual
 889            >>> stock.get_cashflow(quarterly=True)  # Quarterly
 890
 891            >>> bank = bp.Ticker("AKBNK")
 892            >>> bank.get_cashflow(financial_group="UFRS")
 893        """
 894        return self._get_isyatirim().get_financial_statements(
 895            symbol=self._symbol,
 896            statement_type="cashflow",
 897            quarterly=quarterly,
 898            financial_group=financial_group,
 899        )
 900
 901    # Legacy property aliases for backward compatibility
 902    @cached_property
 903    def balance_sheet(self) -> pd.DataFrame:
 904        """Annual balance sheet (use get_balance_sheet() for more options)."""
 905        return self.get_balance_sheet(quarterly=False)
 906
 907    @cached_property
 908    def quarterly_balance_sheet(self) -> pd.DataFrame:
 909        """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options)."""
 910        return self.get_balance_sheet(quarterly=True)
 911
 912    @cached_property
 913    def income_stmt(self) -> pd.DataFrame:
 914        """Annual income statement (use get_income_stmt() for more options)."""
 915        return self.get_income_stmt(quarterly=False)
 916
 917    @cached_property
 918    def quarterly_income_stmt(self) -> pd.DataFrame:
 919        """Quarterly income statement (use get_income_stmt(quarterly=True) for more options)."""
 920        return self.get_income_stmt(quarterly=True)
 921
 922    @cached_property
 923    def cashflow(self) -> pd.DataFrame:
 924        """Annual cash flow (use get_cashflow() for more options)."""
 925        return self.get_cashflow(quarterly=False)
 926
 927    @cached_property
 928    def quarterly_cashflow(self) -> pd.DataFrame:
 929        """Quarterly cash flow (use get_cashflow(quarterly=True) for more options)."""
 930        return self.get_cashflow(quarterly=True)
 931
 932    def _calculate_ttm(self, quarterly_df: pd.DataFrame) -> pd.DataFrame:
 933        """
 934        Calculate trailing twelve months (TTM) by summing last 4 quarters.
 935
 936        Args:
 937            quarterly_df: DataFrame with quarterly data (columns in YYYYQN format).
 938
 939        Returns:
 940            DataFrame with single TTM column containing summed values.
 941        """
 942        if quarterly_df.empty or len(quarterly_df.columns) < 4:
 943            return pd.DataFrame(columns=["TTM"])
 944
 945        # First 4 columns = last 4 quarters (most recent first)
 946        last_4_quarters = quarterly_df.iloc[:, :4]
 947
 948        # Convert to numeric, coercing errors to NaN
 949        numeric_df = last_4_quarters.apply(pd.to_numeric, errors="coerce")
 950
 951        return numeric_df.sum(axis=1).to_frame(name="TTM")
 952
 953    def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame:
 954        """
 955        Get trailing twelve months (TTM) income statement.
 956
 957        Calculates TTM by summing the last 4 quarters of income statement data.
 958
 959        Args:
 960            financial_group: Financial group code. Use "UFRS" for banks,
 961                           "XI_29" for industrial companies. If None, defaults to XI_29.
 962
 963        Returns:
 964            DataFrame with TTM column containing summed values for each line item.
 965
 966        Examples:
 967            >>> stock = bp.Ticker("THYAO")
 968            >>> stock.get_ttm_income_stmt()
 969
 970            >>> bank = bp.Ticker("AKBNK")
 971            >>> bank.get_ttm_income_stmt(financial_group="UFRS")
 972        """
 973        quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group)
 974        return self._calculate_ttm(quarterly)
 975
 976    def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame:
 977        """
 978        Get trailing twelve months (TTM) cash flow statement.
 979
 980        Calculates TTM by summing the last 4 quarters of cash flow data.
 981
 982        Args:
 983            financial_group: Financial group code. Use "UFRS" for banks,
 984                           "XI_29" for industrial companies. If None, defaults to XI_29.
 985
 986        Returns:
 987            DataFrame with TTM column containing summed values for each line item.
 988
 989        Examples:
 990            >>> stock = bp.Ticker("THYAO")
 991            >>> stock.get_ttm_cashflow()
 992
 993            >>> bank = bp.Ticker("AKBNK")
 994            >>> bank.get_ttm_cashflow(financial_group="UFRS")
 995        """
 996        quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group)
 997        return self._calculate_ttm(quarterly)
 998
 999    # Legacy property aliases
1000    @cached_property
1001    def ttm_income_stmt(self) -> pd.DataFrame:
1002        """TTM income statement (use get_ttm_income_stmt() for banks)."""
1003        return self.get_ttm_income_stmt()
1004
1005    @cached_property
1006    def ttm_cashflow(self) -> pd.DataFrame:
1007        """TTM cash flow (use get_ttm_cashflow() for banks)."""
1008        return self.get_ttm_cashflow()
1009
1010    @cached_property
1011    def major_holders(self) -> pd.DataFrame:
1012        """
1013        Get major shareholders (ortaklık yapısı).
1014
1015        Returns:
1016            DataFrame with shareholder names and percentages:
1017            - Index: Holder name
1018            - Percentage: Ownership percentage (%)
1019
1020        Examples:
1021            >>> stock = Ticker("THYAO")
1022            >>> stock.major_holders
1023                                     Percentage
1024            Holder
1025            DiÄŸer                        50.88
1026            Türkiye Varlık Fonu          49.12
1027        """
1028        return self._get_isyatirim().get_major_holders(self._symbol)
1029
1030    @cached_property
1031    def recommendations(self) -> dict:
1032        """
1033        Get analyst recommendations and target price.
1034
1035        Returns:
1036            Dictionary with:
1037            - recommendation: Buy/Hold/Sell (AL/TUT/SAT)
1038            - target_price: Analyst target price (TL)
1039            - upside_potential: Expected upside (%)
1040
1041        Examples:
1042            >>> stock = Ticker("THYAO")
1043            >>> stock.recommendations
1044            {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
1045        """
1046        return self._get_isyatirim().get_recommendations(self._symbol)
1047
1048    @cached_property
1049    def recommendations_summary(self) -> dict[str, int]:
1050        """
1051        Get analyst recommendation summary with buy/hold/sell counts.
1052
1053        Aggregates individual analyst recommendations from hedeffiyat.com.tr
1054        into yfinance-compatible categories.
1055
1056        Returns:
1057            Dictionary with counts:
1058            - strongBuy: Strong buy recommendations
1059            - buy: Buy recommendations (includes "Endeks Üstü Getiri")
1060            - hold: Hold recommendations (includes "Nötr", "Endekse Paralel")
1061            - sell: Sell recommendations (includes "Endeks Altı Getiri")
1062            - strongSell: Strong sell recommendations
1063
1064        Examples:
1065            >>> stock = Ticker("THYAO")
1066            >>> stock.recommendations_summary
1067            {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
1068        """
1069        return self._get_hedeffiyat().get_recommendations_summary(self._symbol)
1070
1071    @cached_property
1072    def news(self) -> pd.DataFrame:
1073        """
1074        Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
1075
1076        Fetches directly from KAP - the official disclosure platform for
1077        publicly traded companies in Turkey.
1078
1079        Returns:
1080            DataFrame with columns:
1081            - Date: Disclosure date and time
1082            - Title: Disclosure headline
1083            - URL: Link to full disclosure on KAP
1084
1085        Examples:
1086            >>> stock = Ticker("THYAO")
1087            >>> stock.news
1088                              Date                                         Title                                         URL
1089            0  29.12.2025 19:21:18  Haber ve Söylentilere İlişkin Açıklama  https://www.kap.org.tr/tr/Bildirim/1530826
1090            1  29.12.2025 16:11:36  Payların Geri Alınmasına İlişkin Bildirim  https://www.kap.org.tr/tr/Bildirim/1530656
1091        """
1092        return self._get_kap().get_disclosures(self._symbol)
1093
1094    def get_news_content(self, disclosure_id: int | str) -> str | None:
1095        """
1096        Get full HTML content of a KAP disclosure by ID.
1097
1098        Args:
1099            disclosure_id: KAP disclosure ID from news DataFrame URL.
1100
1101        Returns:
1102            Raw HTML content or None if failed.
1103
1104        Examples:
1105            >>> stock = Ticker("THYAO")
1106            >>> html = stock.get_news_content(1530826)
1107        """
1108        return self._get_kap().get_disclosure_content(disclosure_id)
1109
1110    @cached_property
1111    def calendar(self) -> pd.DataFrame:
1112        """
1113        Get expected disclosure calendar for the stock from KAP.
1114
1115        Returns upcoming expected disclosures like financial reports,
1116        annual reports, sustainability reports, and corporate governance reports.
1117
1118        Returns:
1119            DataFrame with columns:
1120            - StartDate: Expected disclosure window start
1121            - EndDate: Expected disclosure window end
1122            - Subject: Type of disclosure (e.g., "Finansal Rapor")
1123            - Period: Report period (e.g., "Yıllık", "3 Aylık")
1124            - Year: Fiscal year
1125
1126        Examples:
1127            >>> stock = Ticker("THYAO")
1128            >>> stock.calendar
1129                  StartDate       EndDate               Subject   Period  Year
1130            0  01.01.2026  11.03.2026       Finansal Rapor   Yıllık  2025
1131            1  01.01.2026  11.03.2026    Faaliyet Raporu  Yıllık  2025
1132            2  01.04.2026  11.05.2026       Finansal Rapor  3 Aylık  2026
1133        """
1134        return self._get_kap().get_calendar(self._symbol)
1135
1136    @cached_property
1137    def isin(self) -> str | None:
1138        """
1139        Get ISIN (International Securities Identification Number) code.
1140
1141        ISIN is a 12-character alphanumeric code that uniquely identifies
1142        a security, standardized by ISO 6166.
1143
1144        Returns:
1145            ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
1146
1147        Examples:
1148            >>> stock = Ticker("THYAO")
1149            >>> stock.isin
1150            'TRATHYAO91M5'
1151        """
1152        return self._get_isin_provider().get_isin(self._symbol)
1153
1154    @cached_property
1155    def analyst_price_targets(self) -> dict[str, float | int | None]:
1156        """
1157        Get analyst price target data from hedeffiyat.com.tr.
1158
1159        Returns aggregated price target information from multiple analysts.
1160
1161        Returns:
1162            Dictionary with:
1163            - current: Current stock price
1164            - low: Lowest analyst target price
1165            - high: Highest analyst target price
1166            - mean: Average target price
1167            - median: Median target price
1168            - numberOfAnalysts: Number of analysts covering the stock
1169
1170        Examples:
1171            >>> stock = Ticker("THYAO")
1172            >>> stock.analyst_price_targets
1173            {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49,
1174             'median': 465.0, 'numberOfAnalysts': 19}
1175        """
1176        return self._get_hedeffiyat().get_price_targets(self._symbol)
1177
1178    @property
1179    def etf_holders(self) -> pd.DataFrame:
1180        """
1181        Get international ETFs that hold this stock.
1182
1183        Returns data from TradingView showing which ETFs hold this stock,
1184        including position value, weight, and ETF characteristics.
1185
1186        Returns:
1187            DataFrame with ETF holder information:
1188            - symbol: ETF ticker symbol
1189            - exchange: Exchange (AMEX, NASDAQ, LSE, etc.)
1190            - name: ETF full name
1191            - market_cap_usd: Position value in USD
1192            - holding_weight_pct: Weight percentage (0.09 = 0.09%)
1193            - issuer: ETF issuer (BlackRock, Vanguard, etc.)
1194            - management: Management style (Passive/Active)
1195            - focus: Investment focus (Total Market, Emerging Markets, etc.)
1196            - expense_ratio: Expense ratio (0.09 = 0.09%)
1197            - aum_usd: Total assets under management (USD)
1198            - price: Current ETF price
1199            - change_pct: Change percentage
1200
1201        Examples:
1202            >>> stock = Ticker("ASELS")
1203            >>> holders = stock.etf_holders
1204            >>> holders[['symbol', 'name', 'holding_weight_pct']].head()
1205               symbol                                      name  holding_weight_pct
1206            0    IEMG  iShares Core MSCI Emerging Markets ETF            0.090686
1207            1     VWO     Vanguard FTSE Emerging Markets ETF            0.060000
1208
1209            >>> print(f"Total ETFs: {len(holders)}")
1210            Total ETFs: 118
1211        """
1212        return self._get_etf_provider().get_etf_holders(self._symbol)
1213
1214    @cached_property
1215    def earnings_dates(self) -> pd.DataFrame:
1216        """
1217        Get upcoming earnings announcement dates.
1218
1219        Derived from KAP calendar, showing expected financial report dates.
1220        Compatible with yfinance earnings_dates format.
1221
1222        Returns:
1223            DataFrame with index as Earnings Date and columns:
1224            - EPS Estimate: Always None (not available for BIST)
1225            - Reported EPS: Always None (not available for BIST)
1226            - Surprise (%): Always None (not available for BIST)
1227
1228        Examples:
1229            >>> stock = Ticker("THYAO")
1230            >>> stock.earnings_dates
1231                            EPS Estimate  Reported EPS  Surprise(%)
1232            Earnings Date
1233            2026-03-11           None          None         None
1234            2026-05-11           None          None         None
1235        """
1236        cal = self.calendar
1237        if cal.empty:
1238            return pd.DataFrame(
1239                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1240            )
1241
1242        # Filter for financial reports only
1243        financial_reports = cal[
1244            cal["Subject"].str.contains("Finansal Rapor", case=False, na=False)
1245        ]
1246
1247        if financial_reports.empty:
1248            return pd.DataFrame(
1249                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1250            )
1251
1252        # Use EndDate as the earnings date (latest expected date)
1253        earnings_dates = []
1254        for _, row in financial_reports.iterrows():
1255            end_date = row.get("EndDate", "")
1256            if end_date:
1257                try:
1258                    # Parse Turkish date format (DD.MM.YYYY)
1259                    parsed = datetime.strptime(end_date, "%d.%m.%Y")
1260                    earnings_dates.append(parsed)
1261                except ValueError:
1262                    continue
1263
1264        if not earnings_dates:
1265            return pd.DataFrame(
1266                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1267            )
1268
1269        result = pd.DataFrame(
1270            {
1271                "EPS Estimate": [None] * len(earnings_dates),
1272                "Reported EPS": [None] * len(earnings_dates),
1273                "Surprise(%)": [None] * len(earnings_dates),
1274            },
1275            index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"),
1276        )
1277        result = result.sort_index()
1278        return result
1279
1280    def _parse_date(self, date: str | datetime) -> datetime:
1281        """Parse a date string to datetime."""
1282        if isinstance(date, datetime):
1283            return date
1284        # Try common formats
1285        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
1286            try:
1287                return datetime.strptime(date, fmt)
1288            except ValueError:
1289                continue
1290        raise ValueError(f"Could not parse date: {date}")
1291
1292    def _get_ta_symbol_info(self) -> tuple[str, str]:
1293        """Get TradingView symbol and screener for TA signals.
1294
1295        Returns:
1296            Tuple of (tv_symbol, screener) for TradingView Scanner API.
1297        """
1298        return (f"BIST:{self._symbol}", "turkey")
1299
1300    def __repr__(self) -> str:
1301        return f"Ticker('{self._symbol}')"

A yfinance-like interface for Turkish stock data.

Examples:

import borsapy as bp stock = bp.Ticker("THYAO") stock.info {'symbol': 'THYAO', 'last': 268.5, ...} stock.history(period="1mo") Open High Low Close Volume Date 2024-12-01 265.00 268.00 264.00 267.50 12345678 ...

Ticker(symbol: str)
484    def __init__(self, symbol: str):
485        """
486        Initialize a Ticker object.
487
488        Args:
489            symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS").
490                    The ".IS" or ".E" suffix is optional and will be removed.
491        """
492        self._symbol = symbol.upper().replace(".IS", "").replace(".E", "")
493        self._tradingview = get_tradingview_provider()
494        self._isyatirim = None  # Lazy load for financial statements
495        self._kap = None  # Lazy load for KAP disclosures
496        self._isin_provider = None  # Lazy load for ISIN lookup
497        self._hedeffiyat = None  # Lazy load for analyst price targets
498        self._etf_provider = None  # Lazy load for ETF holders

Initialize a Ticker object.

Args: symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS"). The ".IS" or ".E" suffix is optional and will be removed.

symbol: str
540    @property
541    def symbol(self) -> str:
542        """Return the ticker symbol."""
543        return self._symbol

Return the ticker symbol.

fast_info: borsapy.ticker.FastInfo
545    @property
546    def fast_info(self) -> FastInfo:
547        """
548        Get fast access to common ticker information.
549
550        Returns a FastInfo object with quick access to frequently used data:
551        - currency, exchange, timezone
552        - last_price, open, day_high, day_low, previous_close, volume
553        - market_cap, shares, pe_ratio, pb_ratio
554        - year_high, year_low (52-week)
555        - fifty_day_average, two_hundred_day_average
556        - free_float, foreign_ratio
557
558        Examples:
559            >>> stock = Ticker("THYAO")
560            >>> stock.fast_info.market_cap
561            370530000000
562            >>> stock.fast_info['pe_ratio']
563            2.8
564            >>> stock.fast_info.keys()
565            ['currency', 'exchange', 'timezone', ...]
566        """
567        if not hasattr(self, "_fast_info"):
568            self._fast_info = FastInfo(self)
569        return self._fast_info

Get fast access to common ticker information.

Returns a FastInfo object with quick access to frequently used data:

  • currency, exchange, timezone
  • last_price, open, day_high, day_low, previous_close, volume
  • market_cap, shares, pe_ratio, pb_ratio
  • year_high, year_low (52-week)
  • fifty_day_average, two_hundred_day_average
  • free_float, foreign_ratio

Examples:

stock = Ticker("THYAO") stock.fast_info.market_cap 370530000000 stock.fast_info['pe_ratio'] 2.8 stock.fast_info.keys() ['currency', 'exchange', 'timezone', ...]

info: borsapy.ticker.EnrichedInfo
571    @property
572    def info(self) -> EnrichedInfo:
573        """
574        Get comprehensive ticker information with yfinance-compatible fields.
575
576        Returns:
577            EnrichedInfo object providing dict-like access to:
578
579            Basic fields (always loaded, fast):
580            - symbol, last, open, high, low, close, volume
581            - change, change_percent, update_time
582
583            yfinance aliases (map to basic fields):
584            - regularMarketPrice, currentPrice -> last
585            - regularMarketOpen -> open
586            - regularMarketDayHigh -> high
587            - regularMarketDayLow -> low
588            - regularMarketPreviousClose -> close
589            - regularMarketVolume -> volume
590
591            Extended fields (lazy-loaded on access):
592            - marketCap, trailingPE, priceToBook, enterpriseToEbitda
593            - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
594            - fiftyDayAverage, twoHundredDayAverage
595            - floatShares, foreignRatio, netDebt
596            - currency, exchange, timezone
597
598            Dividend fields (lazy-loaded on access):
599            - dividendYield, exDividendDate
600            - trailingAnnualDividendRate, trailingAnnualDividendYield
601
602        Examples:
603            >>> stock = Ticker("THYAO")
604            >>> stock.info['last']  # Basic field - fast
605            268.5
606            >>> stock.info['marketCap']  # Extended field - fetches İş Yatırım
607            370530000000
608            >>> stock.info['trailingPE']  # yfinance compatible name
609            2.8
610            >>> stock.info.get('dividendYield')  # Safe access
611            1.28
612            >>> stock.info.todict()  # Get all as regular dict
613            {...}
614        """
615        if not hasattr(self, "_enriched_info"):
616            self._enriched_info = EnrichedInfo(self)
617        return self._enriched_info

Get comprehensive ticker information with yfinance-compatible fields.

Returns: EnrichedInfo object providing dict-like access to:

Basic fields (always loaded, fast):
- symbol, last, open, high, low, close, volume
- change, change_percent, update_time

yfinance aliases (map to basic fields):
- regularMarketPrice, currentPrice -> last
- regularMarketOpen -> open
- regularMarketDayHigh -> high
- regularMarketDayLow -> low
- regularMarketPreviousClose -> close
- regularMarketVolume -> volume

Extended fields (lazy-loaded on access):
- marketCap, trailingPE, priceToBook, enterpriseToEbitda
- sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
- fiftyDayAverage, twoHundredDayAverage
- floatShares, foreignRatio, netDebt
- currency, exchange, timezone

Dividend fields (lazy-loaded on access):
- dividendYield, exDividendDate
- trailingAnnualDividendRate, trailingAnnualDividendYield

Examples:

stock = Ticker("THYAO") stock.info['last'] # Basic field - fast 268.5 stock.info['marketCap'] # Extended field - fetches İş Yatırım 370530000000 stock.info['trailingPE'] # yfinance compatible name 2.8 stock.info.get('dividendYield') # Safe access 1.28 stock.info.todict() # Get all as regular dict {...}

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, actions: bool = False) -> pandas.core.frame.DataFrame:
619    def history(
620        self,
621        period: str = "1mo",
622        interval: str = "1d",
623        start: datetime | str | None = None,
624        end: datetime | str | None = None,
625        actions: bool = False,
626    ) -> pd.DataFrame:
627        """
628        Get historical OHLCV data.
629
630        Args:
631            period: How much data to fetch. Valid periods:
632                    1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
633                    Ignored if start is provided.
634            interval: Data granularity. Valid intervals:
635                      1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo.
636            start: Start date (string or datetime).
637            end: End date (string or datetime). Defaults to today.
638            actions: If True, include Dividends and Stock Splits columns.
639                     Defaults to False.
640
641        Returns:
642            DataFrame with columns: Open, High, Low, Close, Volume.
643            If actions=True, also includes Dividends and Stock Splits columns.
644            Index is the Date.
645
646        Examples:
647            >>> stock = Ticker("THYAO")
648            >>> stock.history(period="1mo")  # Last month
649            >>> stock.history(period="1y", interval="1wk")  # Weekly for 1 year
650            >>> stock.history(start="2024-01-01", end="2024-06-30")  # Date range
651            >>> stock.history(period="1y", actions=True)  # With dividends/splits
652        """
653        # Parse dates if strings
654        start_dt = self._parse_date(start) if start else None
655        end_dt = self._parse_date(end) if end else None
656
657        df = self._tradingview.get_history(
658            symbol=self._symbol,
659            period=period,
660            interval=interval,
661            start=start_dt,
662            end=end_dt,
663        )
664
665        if actions and not df.empty:
666            df = self._add_actions_to_history(df)
667
668        return df

Get historical OHLCV data.

Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max. Ignored if start is provided. interval: Data granularity. Valid intervals: 1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today. actions: If True, include Dividends and Stock Splits columns. Defaults to False.

Returns: DataFrame with columns: Open, High, Low, Close, Volume. If actions=True, also includes Dividends and Stock Splits columns. Index is the Date.

Examples:

stock = Ticker("THYAO") stock.history(period="1mo") # Last month stock.history(period="1y", interval="1wk") # Weekly for 1 year stock.history(start="2024-01-01", end="2024-06-30") # Date range stock.history(period="1y", actions=True) # With dividends/splits

dividends: pandas.core.frame.DataFrame
725    @cached_property
726    def dividends(self) -> pd.DataFrame:
727        """
728        Get dividend history.
729
730        Returns:
731            DataFrame with dividend history:
732            - Amount: Dividend per share (TL)
733            - GrossRate: Gross dividend rate (%)
734            - NetRate: Net dividend rate (%)
735            - TotalDividend: Total dividend distributed (TL)
736
737        Examples:
738            >>> stock = Ticker("THYAO")
739            >>> stock.dividends
740                           Amount  GrossRate  NetRate  TotalDividend
741            Date
742            2025-09-02     3.442    344.20   292.57  4750000000.0
743            2025-06-16     3.442    344.20   292.57  4750000000.0
744        """
745        return self._get_isyatirim().get_dividends(self._symbol)

Get dividend history.

Returns: DataFrame with dividend history: - Amount: Dividend per share (TL) - GrossRate: Gross dividend rate (%) - NetRate: Net dividend rate (%) - TotalDividend: Total dividend distributed (TL)

Examples:

stock = Ticker("THYAO") stock.dividends Amount GrossRate NetRate TotalDividend Date 2025-09-02 3.442 344.20 292.57 4750000000.0 2025-06-16 3.442 344.20 292.57 4750000000.0

splits: pandas.core.frame.DataFrame
747    @cached_property
748    def splits(self) -> pd.DataFrame:
749        """
750        Get capital increase (split) history.
751
752        Note: Turkish market uses capital increases instead of traditional splits.
753        - RightsIssue: Paid capital increase (bedelli)
754        - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
755        - BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
756
757        Returns:
758            DataFrame with capital increase history:
759            - Capital: New capital after increase (TL)
760            - RightsIssue: Rights issue rate (%)
761            - BonusFromCapital: Bonus from capital (%)
762            - BonusFromDividend: Bonus from dividend (%)
763
764        Examples:
765            >>> stock = Ticker("THYAO")
766            >>> stock.splits
767                             Capital  RightsIssue  BonusFromCapital  BonusFromDividend
768            Date
769            2013-06-26  1380000000.0         0.0             15.00               0.0
770            2011-07-11  1200000000.0         0.0              0.00              20.0
771        """
772        return self._get_isyatirim().get_capital_increases(self._symbol)

Get capital increase (split) history.

Note: Turkish market uses capital increases instead of traditional splits.

  • RightsIssue: Paid capital increase (bedelli)
  • BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
  • BonusFromDividend: Free shares from dividend (bedelsiz temettüden)

Returns: DataFrame with capital increase history: - Capital: New capital after increase (TL) - RightsIssue: Rights issue rate (%) - BonusFromCapital: Bonus from capital (%) - BonusFromDividend: Bonus from dividend (%)

Examples:

stock = Ticker("THYAO") stock.splits Capital RightsIssue BonusFromCapital BonusFromDividend Date 2013-06-26 1380000000.0 0.0 15.00 0.0 2011-07-11 1200000000.0 0.0 0.00 20.0

actions: pandas.core.frame.DataFrame
774    @cached_property
775    def actions(self) -> pd.DataFrame:
776        """
777        Get combined dividends and splits history.
778
779        Returns:
780            DataFrame with combined dividend and split actions:
781            - Dividends: Dividend per share (TL) or 0
782            - Splits: Combined split ratio (0 if no split)
783
784        Examples:
785            >>> stock = Ticker("THYAO")
786            >>> stock.actions
787                         Dividends  Splits
788            Date
789            2025-09-02      3.442    0.0
790            2013-06-26      0.000   15.0
791        """
792        dividends = self.dividends
793        splits = self.splits
794
795        # Merge on index (Date)
796        if dividends.empty and splits.empty:
797            return pd.DataFrame(columns=["Dividends", "Splits"])
798
799        # Extract relevant columns
800        div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float)
801        split_series = (
802            splits["BonusFromCapital"] + splits["BonusFromDividend"]
803            if not splits.empty
804            else pd.Series(dtype=float)
805        )
806
807        # Combine into single DataFrame
808        result = pd.DataFrame({"Dividends": div_series, "Splits": split_series})
809        result = result.fillna(0)
810        result = result.sort_index(ascending=False)
811
812        return result

Get combined dividends and splits history.

Returns: DataFrame with combined dividend and split actions: - Dividends: Dividend per share (TL) or 0 - Splits: Combined split ratio (0 if no split)

Examples:

stock = Ticker("THYAO") stock.actions Dividends Splits Date 2025-09-02 3.442 0.0 2013-06-26 0.000 15.0

def get_balance_sheet( self, quarterly: bool = False, financial_group: str | None = None) -> pandas.core.frame.DataFrame:
814    def get_balance_sheet(
815        self, quarterly: bool = False, financial_group: str | None = None
816    ) -> pd.DataFrame:
817        """
818        Get balance sheet data.
819
820        Args:
821            quarterly: If True, return quarterly data. If False, return annual.
822            financial_group: Financial group code. Use "UFRS" for banks,
823                           "XI_29" for industrial companies. If None, defaults to XI_29.
824
825        Returns:
826            DataFrame with balance sheet items as rows and periods as columns.
827
828        Examples:
829            >>> stock = bp.Ticker("THYAO")
830            >>> stock.get_balance_sheet()  # Annual, industrial
831            >>> stock.get_balance_sheet(quarterly=True)  # Quarterly
832
833            >>> bank = bp.Ticker("AKBNK")
834            >>> bank.get_balance_sheet(financial_group="UFRS")  # Banks need UFRS
835        """
836        return self._get_isyatirim().get_financial_statements(
837            symbol=self._symbol,
838            statement_type="balance_sheet",
839            quarterly=quarterly,
840            financial_group=financial_group,
841        )

Get balance sheet data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.

Returns: DataFrame with balance sheet items as rows and periods as columns.

Examples:

stock = bp.Ticker("THYAO") stock.get_balance_sheet() # Annual, industrial stock.get_balance_sheet(quarterly=True) # Quarterly

>>> bank = bp.Ticker("AKBNK")
>>> bank.get_balance_sheet(financial_group="UFRS")  # Banks need UFRS
def get_income_stmt( self, quarterly: bool = False, financial_group: str | None = None) -> pandas.core.frame.DataFrame:
843    def get_income_stmt(
844        self, quarterly: bool = False, financial_group: str | None = None
845    ) -> pd.DataFrame:
846        """
847        Get income statement data.
848
849        Args:
850            quarterly: If True, return quarterly data. If False, return annual.
851            financial_group: Financial group code. Use "UFRS" for banks,
852                           "XI_29" for industrial companies. If None, defaults to XI_29.
853
854        Returns:
855            DataFrame with income statement items as rows and periods as columns.
856
857        Examples:
858            >>> stock = bp.Ticker("THYAO")
859            >>> stock.get_income_stmt()  # Annual
860            >>> stock.get_income_stmt(quarterly=True)  # Quarterly
861
862            >>> bank = bp.Ticker("AKBNK")
863            >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
864        """
865        return self._get_isyatirim().get_financial_statements(
866            symbol=self._symbol,
867            statement_type="income_stmt",
868            quarterly=quarterly,
869            financial_group=financial_group,
870        )

Get income statement data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.

Returns: DataFrame with income statement items as rows and periods as columns.

Examples:

stock = bp.Ticker("THYAO") stock.get_income_stmt() # Annual stock.get_income_stmt(quarterly=True) # Quarterly

>>> bank = bp.Ticker("AKBNK")
>>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
def get_cashflow( self, quarterly: bool = False, financial_group: str | None = None) -> pandas.core.frame.DataFrame:
872    def get_cashflow(
873        self, quarterly: bool = False, financial_group: str | None = None
874    ) -> pd.DataFrame:
875        """
876        Get cash flow statement data.
877
878        Args:
879            quarterly: If True, return quarterly data. If False, return annual.
880            financial_group: Financial group code. Use "UFRS" for banks,
881                           "XI_29" for industrial companies. If None, defaults to XI_29.
882
883        Returns:
884            DataFrame with cash flow items as rows and periods as columns.
885
886        Examples:
887            >>> stock = bp.Ticker("THYAO")
888            >>> stock.get_cashflow()  # Annual
889            >>> stock.get_cashflow(quarterly=True)  # Quarterly
890
891            >>> bank = bp.Ticker("AKBNK")
892            >>> bank.get_cashflow(financial_group="UFRS")
893        """
894        return self._get_isyatirim().get_financial_statements(
895            symbol=self._symbol,
896            statement_type="cashflow",
897            quarterly=quarterly,
898            financial_group=financial_group,
899        )

Get cash flow statement data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.

Returns: DataFrame with cash flow items as rows and periods as columns.

Examples:

stock = bp.Ticker("THYAO") stock.get_cashflow() # Annual stock.get_cashflow(quarterly=True) # Quarterly

>>> bank = bp.Ticker("AKBNK")
>>> bank.get_cashflow(financial_group="UFRS")
balance_sheet: pandas.core.frame.DataFrame
902    @cached_property
903    def balance_sheet(self) -> pd.DataFrame:
904        """Annual balance sheet (use get_balance_sheet() for more options)."""
905        return self.get_balance_sheet(quarterly=False)

Annual balance sheet (use get_balance_sheet() for more options).

quarterly_balance_sheet: pandas.core.frame.DataFrame
907    @cached_property
908    def quarterly_balance_sheet(self) -> pd.DataFrame:
909        """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options)."""
910        return self.get_balance_sheet(quarterly=True)

Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options).

income_stmt: pandas.core.frame.DataFrame
912    @cached_property
913    def income_stmt(self) -> pd.DataFrame:
914        """Annual income statement (use get_income_stmt() for more options)."""
915        return self.get_income_stmt(quarterly=False)

Annual income statement (use get_income_stmt() for more options).

quarterly_income_stmt: pandas.core.frame.DataFrame
917    @cached_property
918    def quarterly_income_stmt(self) -> pd.DataFrame:
919        """Quarterly income statement (use get_income_stmt(quarterly=True) for more options)."""
920        return self.get_income_stmt(quarterly=True)

Quarterly income statement (use get_income_stmt(quarterly=True) for more options).

cashflow: pandas.core.frame.DataFrame
922    @cached_property
923    def cashflow(self) -> pd.DataFrame:
924        """Annual cash flow (use get_cashflow() for more options)."""
925        return self.get_cashflow(quarterly=False)

Annual cash flow (use get_cashflow() for more options).

quarterly_cashflow: pandas.core.frame.DataFrame
927    @cached_property
928    def quarterly_cashflow(self) -> pd.DataFrame:
929        """Quarterly cash flow (use get_cashflow(quarterly=True) for more options)."""
930        return self.get_cashflow(quarterly=True)

Quarterly cash flow (use get_cashflow(quarterly=True) for more options).

def get_ttm_income_stmt(self, financial_group: str | None = None) -> pandas.core.frame.DataFrame:
953    def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame:
954        """
955        Get trailing twelve months (TTM) income statement.
956
957        Calculates TTM by summing the last 4 quarters of income statement data.
958
959        Args:
960            financial_group: Financial group code. Use "UFRS" for banks,
961                           "XI_29" for industrial companies. If None, defaults to XI_29.
962
963        Returns:
964            DataFrame with TTM column containing summed values for each line item.
965
966        Examples:
967            >>> stock = bp.Ticker("THYAO")
968            >>> stock.get_ttm_income_stmt()
969
970            >>> bank = bp.Ticker("AKBNK")
971            >>> bank.get_ttm_income_stmt(financial_group="UFRS")
972        """
973        quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group)
974        return self._calculate_ttm(quarterly)

Get trailing twelve months (TTM) income statement.

Calculates TTM by summing the last 4 quarters of income statement data.

Args: financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.

Returns: DataFrame with TTM column containing summed values for each line item.

Examples:

stock = bp.Ticker("THYAO") stock.get_ttm_income_stmt()

>>> bank = bp.Ticker("AKBNK")
>>> bank.get_ttm_income_stmt(financial_group="UFRS")
def get_ttm_cashflow(self, financial_group: str | None = None) -> pandas.core.frame.DataFrame:
976    def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame:
977        """
978        Get trailing twelve months (TTM) cash flow statement.
979
980        Calculates TTM by summing the last 4 quarters of cash flow data.
981
982        Args:
983            financial_group: Financial group code. Use "UFRS" for banks,
984                           "XI_29" for industrial companies. If None, defaults to XI_29.
985
986        Returns:
987            DataFrame with TTM column containing summed values for each line item.
988
989        Examples:
990            >>> stock = bp.Ticker("THYAO")
991            >>> stock.get_ttm_cashflow()
992
993            >>> bank = bp.Ticker("AKBNK")
994            >>> bank.get_ttm_cashflow(financial_group="UFRS")
995        """
996        quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group)
997        return self._calculate_ttm(quarterly)

Get trailing twelve months (TTM) cash flow statement.

Calculates TTM by summing the last 4 quarters of cash flow data.

Args: financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29.

Returns: DataFrame with TTM column containing summed values for each line item.

Examples:

stock = bp.Ticker("THYAO") stock.get_ttm_cashflow()

>>> bank = bp.Ticker("AKBNK")
>>> bank.get_ttm_cashflow(financial_group="UFRS")
ttm_income_stmt: pandas.core.frame.DataFrame
1000    @cached_property
1001    def ttm_income_stmt(self) -> pd.DataFrame:
1002        """TTM income statement (use get_ttm_income_stmt() for banks)."""
1003        return self.get_ttm_income_stmt()

TTM income statement (use get_ttm_income_stmt() for banks).

ttm_cashflow: pandas.core.frame.DataFrame
1005    @cached_property
1006    def ttm_cashflow(self) -> pd.DataFrame:
1007        """TTM cash flow (use get_ttm_cashflow() for banks)."""
1008        return self.get_ttm_cashflow()

TTM cash flow (use get_ttm_cashflow() for banks).

major_holders: pandas.core.frame.DataFrame
1010    @cached_property
1011    def major_holders(self) -> pd.DataFrame:
1012        """
1013        Get major shareholders (ortaklık yapısı).
1014
1015        Returns:
1016            DataFrame with shareholder names and percentages:
1017            - Index: Holder name
1018            - Percentage: Ownership percentage (%)
1019
1020        Examples:
1021            >>> stock = Ticker("THYAO")
1022            >>> stock.major_holders
1023                                     Percentage
1024            Holder
1025            DiÄŸer                        50.88
1026            Türkiye Varlık Fonu          49.12
1027        """
1028        return self._get_isyatirim().get_major_holders(self._symbol)

Get major shareholders (ortaklık yapısı).

Returns: DataFrame with shareholder names and percentages: - Index: Holder name - Percentage: Ownership percentage (%)

Examples:

stock = Ticker("THYAO") stock.major_holders Percentage Holder Diğer 50.88 Türkiye Varlık Fonu 49.12

recommendations: dict
1030    @cached_property
1031    def recommendations(self) -> dict:
1032        """
1033        Get analyst recommendations and target price.
1034
1035        Returns:
1036            Dictionary with:
1037            - recommendation: Buy/Hold/Sell (AL/TUT/SAT)
1038            - target_price: Analyst target price (TL)
1039            - upside_potential: Expected upside (%)
1040
1041        Examples:
1042            >>> stock = Ticker("THYAO")
1043            >>> stock.recommendations
1044            {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
1045        """
1046        return self._get_isyatirim().get_recommendations(self._symbol)

Get analyst recommendations and target price.

Returns: Dictionary with: - recommendation: Buy/Hold/Sell (AL/TUT/SAT) - target_price: Analyst target price (TL) - upside_potential: Expected upside (%)

Examples:

stock = Ticker("THYAO") stock.recommendations {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}

recommendations_summary: dict[str, int]
1048    @cached_property
1049    def recommendations_summary(self) -> dict[str, int]:
1050        """
1051        Get analyst recommendation summary with buy/hold/sell counts.
1052
1053        Aggregates individual analyst recommendations from hedeffiyat.com.tr
1054        into yfinance-compatible categories.
1055
1056        Returns:
1057            Dictionary with counts:
1058            - strongBuy: Strong buy recommendations
1059            - buy: Buy recommendations (includes "Endeks Üstü Getiri")
1060            - hold: Hold recommendations (includes "Nötr", "Endekse Paralel")
1061            - sell: Sell recommendations (includes "Endeks Altı Getiri")
1062            - strongSell: Strong sell recommendations
1063
1064        Examples:
1065            >>> stock = Ticker("THYAO")
1066            >>> stock.recommendations_summary
1067            {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
1068        """
1069        return self._get_hedeffiyat().get_recommendations_summary(self._symbol)

Get analyst recommendation summary with buy/hold/sell counts.

Aggregates individual analyst recommendations from hedeffiyat.com.tr into yfinance-compatible categories.

Returns: Dictionary with counts: - strongBuy: Strong buy recommendations - buy: Buy recommendations (includes "Endeks Üstü Getiri") - hold: Hold recommendations (includes "Nötr", "Endekse Paralel") - sell: Sell recommendations (includes "Endeks Altı Getiri") - strongSell: Strong sell recommendations

Examples:

stock = Ticker("THYAO") stock.recommendations_summary {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}

news: pandas.core.frame.DataFrame
1071    @cached_property
1072    def news(self) -> pd.DataFrame:
1073        """
1074        Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
1075
1076        Fetches directly from KAP - the official disclosure platform for
1077        publicly traded companies in Turkey.
1078
1079        Returns:
1080            DataFrame with columns:
1081            - Date: Disclosure date and time
1082            - Title: Disclosure headline
1083            - URL: Link to full disclosure on KAP
1084
1085        Examples:
1086            >>> stock = Ticker("THYAO")
1087            >>> stock.news
1088                              Date                                         Title                                         URL
1089            0  29.12.2025 19:21:18  Haber ve Söylentilere İlişkin Açıklama  https://www.kap.org.tr/tr/Bildirim/1530826
1090            1  29.12.2025 16:11:36  Payların Geri Alınmasına İlişkin Bildirim  https://www.kap.org.tr/tr/Bildirim/1530656
1091        """
1092        return self._get_kap().get_disclosures(self._symbol)

Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.

Fetches directly from KAP - the official disclosure platform for publicly traded companies in Turkey.

Returns: DataFrame with columns: - Date: Disclosure date and time - Title: Disclosure headline - URL: Link to full disclosure on KAP

Examples:

stock = Ticker("THYAO") stock.news Date Title URL 0 29.12.2025 19:21:18 Haber ve Söylentilere İlişkin Açıklama https://www.kap.org.tr/tr/Bildirim/1530826 1 29.12.2025 16:11:36 Payların Geri Alınmasına İlişkin Bildirim https://www.kap.org.tr/tr/Bildirim/1530656

def get_news_content(self, disclosure_id: int | str) -> str | None:
1094    def get_news_content(self, disclosure_id: int | str) -> str | None:
1095        """
1096        Get full HTML content of a KAP disclosure by ID.
1097
1098        Args:
1099            disclosure_id: KAP disclosure ID from news DataFrame URL.
1100
1101        Returns:
1102            Raw HTML content or None if failed.
1103
1104        Examples:
1105            >>> stock = Ticker("THYAO")
1106            >>> html = stock.get_news_content(1530826)
1107        """
1108        return self._get_kap().get_disclosure_content(disclosure_id)

Get full HTML content of a KAP disclosure by ID.

Args: disclosure_id: KAP disclosure ID from news DataFrame URL.

Returns: Raw HTML content or None if failed.

Examples:

stock = Ticker("THYAO") html = stock.get_news_content(1530826)

calendar: pandas.core.frame.DataFrame
1110    @cached_property
1111    def calendar(self) -> pd.DataFrame:
1112        """
1113        Get expected disclosure calendar for the stock from KAP.
1114
1115        Returns upcoming expected disclosures like financial reports,
1116        annual reports, sustainability reports, and corporate governance reports.
1117
1118        Returns:
1119            DataFrame with columns:
1120            - StartDate: Expected disclosure window start
1121            - EndDate: Expected disclosure window end
1122            - Subject: Type of disclosure (e.g., "Finansal Rapor")
1123            - Period: Report period (e.g., "Yıllık", "3 Aylık")
1124            - Year: Fiscal year
1125
1126        Examples:
1127            >>> stock = Ticker("THYAO")
1128            >>> stock.calendar
1129                  StartDate       EndDate               Subject   Period  Year
1130            0  01.01.2026  11.03.2026       Finansal Rapor   Yıllık  2025
1131            1  01.01.2026  11.03.2026    Faaliyet Raporu  Yıllık  2025
1132            2  01.04.2026  11.05.2026       Finansal Rapor  3 Aylık  2026
1133        """
1134        return self._get_kap().get_calendar(self._symbol)

Get expected disclosure calendar for the stock from KAP.

Returns upcoming expected disclosures like financial reports, annual reports, sustainability reports, and corporate governance reports.

Returns: DataFrame with columns: - StartDate: Expected disclosure window start - EndDate: Expected disclosure window end - Subject: Type of disclosure (e.g., "Finansal Rapor") - Period: Report period (e.g., "Yıllık", "3 Aylık") - Year: Fiscal year

Examples:

stock = Ticker("THYAO") stock.calendar StartDate EndDate Subject Period Year 0 01.01.2026 11.03.2026 Finansal Rapor Yıllık 2025 1 01.01.2026 11.03.2026 Faaliyet Raporu Yıllık 2025 2 01.04.2026 11.05.2026 Finansal Rapor 3 Aylık 2026

isin: str | None
1136    @cached_property
1137    def isin(self) -> str | None:
1138        """
1139        Get ISIN (International Securities Identification Number) code.
1140
1141        ISIN is a 12-character alphanumeric code that uniquely identifies
1142        a security, standardized by ISO 6166.
1143
1144        Returns:
1145            ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
1146
1147        Examples:
1148            >>> stock = Ticker("THYAO")
1149            >>> stock.isin
1150            'TRATHYAO91M5'
1151        """
1152        return self._get_isin_provider().get_isin(self._symbol)

Get ISIN (International Securities Identification Number) code.

ISIN is a 12-character alphanumeric code that uniquely identifies a security, standardized by ISO 6166.

Returns: ISIN code string (e.g., "TRATHYAO91M5") or None if not found.

Examples:

stock = Ticker("THYAO") stock.isin 'TRATHYAO91M5'

analyst_price_targets: dict[str, float | int | None]
1154    @cached_property
1155    def analyst_price_targets(self) -> dict[str, float | int | None]:
1156        """
1157        Get analyst price target data from hedeffiyat.com.tr.
1158
1159        Returns aggregated price target information from multiple analysts.
1160
1161        Returns:
1162            Dictionary with:
1163            - current: Current stock price
1164            - low: Lowest analyst target price
1165            - high: Highest analyst target price
1166            - mean: Average target price
1167            - median: Median target price
1168            - numberOfAnalysts: Number of analysts covering the stock
1169
1170        Examples:
1171            >>> stock = Ticker("THYAO")
1172            >>> stock.analyst_price_targets
1173            {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49,
1174             'median': 465.0, 'numberOfAnalysts': 19}
1175        """
1176        return self._get_hedeffiyat().get_price_targets(self._symbol)

Get analyst price target data from hedeffiyat.com.tr.

Returns aggregated price target information from multiple analysts.

Returns: Dictionary with: - current: Current stock price - low: Lowest analyst target price - high: Highest analyst target price - mean: Average target price - median: Median target price - numberOfAnalysts: Number of analysts covering the stock

Examples:

stock = Ticker("THYAO") stock.analyst_price_targets {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49, 'median': 465.0, 'numberOfAnalysts': 19}

etf_holders: pandas.core.frame.DataFrame
1178    @property
1179    def etf_holders(self) -> pd.DataFrame:
1180        """
1181        Get international ETFs that hold this stock.
1182
1183        Returns data from TradingView showing which ETFs hold this stock,
1184        including position value, weight, and ETF characteristics.
1185
1186        Returns:
1187            DataFrame with ETF holder information:
1188            - symbol: ETF ticker symbol
1189            - exchange: Exchange (AMEX, NASDAQ, LSE, etc.)
1190            - name: ETF full name
1191            - market_cap_usd: Position value in USD
1192            - holding_weight_pct: Weight percentage (0.09 = 0.09%)
1193            - issuer: ETF issuer (BlackRock, Vanguard, etc.)
1194            - management: Management style (Passive/Active)
1195            - focus: Investment focus (Total Market, Emerging Markets, etc.)
1196            - expense_ratio: Expense ratio (0.09 = 0.09%)
1197            - aum_usd: Total assets under management (USD)
1198            - price: Current ETF price
1199            - change_pct: Change percentage
1200
1201        Examples:
1202            >>> stock = Ticker("ASELS")
1203            >>> holders = stock.etf_holders
1204            >>> holders[['symbol', 'name', 'holding_weight_pct']].head()
1205               symbol                                      name  holding_weight_pct
1206            0    IEMG  iShares Core MSCI Emerging Markets ETF            0.090686
1207            1     VWO     Vanguard FTSE Emerging Markets ETF            0.060000
1208
1209            >>> print(f"Total ETFs: {len(holders)}")
1210            Total ETFs: 118
1211        """
1212        return self._get_etf_provider().get_etf_holders(self._symbol)

Get international ETFs that hold this stock.

Returns data from TradingView showing which ETFs hold this stock, including position value, weight, and ETF characteristics.

Returns: DataFrame with ETF holder information: - symbol: ETF ticker symbol - exchange: Exchange (AMEX, NASDAQ, LSE, etc.) - name: ETF full name - market_cap_usd: Position value in USD - holding_weight_pct: Weight percentage (0.09 = 0.09%) - issuer: ETF issuer (BlackRock, Vanguard, etc.) - management: Management style (Passive/Active) - focus: Investment focus (Total Market, Emerging Markets, etc.) - expense_ratio: Expense ratio (0.09 = 0.09%) - aum_usd: Total assets under management (USD) - price: Current ETF price - change_pct: Change percentage

Examples:

stock = Ticker("ASELS") holders = stock.etf_holders holders[['symbol', 'name', 'holding_weight_pct']].head() symbol name holding_weight_pct 0 IEMG iShares Core MSCI Emerging Markets ETF 0.090686 1 VWO Vanguard FTSE Emerging Markets ETF 0.060000

>>> print(f"Total ETFs: {len(holders)}")
Total ETFs: 118
earnings_dates: pandas.core.frame.DataFrame
1214    @cached_property
1215    def earnings_dates(self) -> pd.DataFrame:
1216        """
1217        Get upcoming earnings announcement dates.
1218
1219        Derived from KAP calendar, showing expected financial report dates.
1220        Compatible with yfinance earnings_dates format.
1221
1222        Returns:
1223            DataFrame with index as Earnings Date and columns:
1224            - EPS Estimate: Always None (not available for BIST)
1225            - Reported EPS: Always None (not available for BIST)
1226            - Surprise (%): Always None (not available for BIST)
1227
1228        Examples:
1229            >>> stock = Ticker("THYAO")
1230            >>> stock.earnings_dates
1231                            EPS Estimate  Reported EPS  Surprise(%)
1232            Earnings Date
1233            2026-03-11           None          None         None
1234            2026-05-11           None          None         None
1235        """
1236        cal = self.calendar
1237        if cal.empty:
1238            return pd.DataFrame(
1239                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1240            )
1241
1242        # Filter for financial reports only
1243        financial_reports = cal[
1244            cal["Subject"].str.contains("Finansal Rapor", case=False, na=False)
1245        ]
1246
1247        if financial_reports.empty:
1248            return pd.DataFrame(
1249                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1250            )
1251
1252        # Use EndDate as the earnings date (latest expected date)
1253        earnings_dates = []
1254        for _, row in financial_reports.iterrows():
1255            end_date = row.get("EndDate", "")
1256            if end_date:
1257                try:
1258                    # Parse Turkish date format (DD.MM.YYYY)
1259                    parsed = datetime.strptime(end_date, "%d.%m.%Y")
1260                    earnings_dates.append(parsed)
1261                except ValueError:
1262                    continue
1263
1264        if not earnings_dates:
1265            return pd.DataFrame(
1266                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
1267            )
1268
1269        result = pd.DataFrame(
1270            {
1271                "EPS Estimate": [None] * len(earnings_dates),
1272                "Reported EPS": [None] * len(earnings_dates),
1273                "Surprise(%)": [None] * len(earnings_dates),
1274            },
1275            index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"),
1276        )
1277        result = result.sort_index()
1278        return result

Get upcoming earnings announcement dates.

Derived from KAP calendar, showing expected financial report dates. Compatible with yfinance earnings_dates format.

Returns: DataFrame with index as Earnings Date and columns: - EPS Estimate: Always None (not available for BIST) - Reported EPS: Always None (not available for BIST) - Surprise (%): Always None (not available for BIST)

Examples:

stock = Ticker("THYAO") stock.earnings_dates EPS Estimate Reported EPS Surprise(%) Earnings Date 2026-03-11 None None None 2026-05-11 None None None

class Tickers:
 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'])
Tickers(symbols: str | list[str])
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"]

symbols: list[str]
45    @property
46    def symbols(self) -> list[str]:
47        """Return list of symbols."""
48        return self._symbols.copy()

Return list of symbols.

tickers: dict[str, Ticker]
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.

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, group_by: str = 'column') -> pandas.core.frame.DataFrame:
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.

class FX(borsapy.technical.TechnicalMixin):
 73class FX(TechnicalMixin):
 74    """
 75    A yfinance-like interface for forex and commodity data.
 76
 77    Supported assets:
 78    - Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD (+ 58 more via canlidoviz)
 79    - Precious Metals: gram-altin, gumus, ons-altin, gram-platin, XAG-USD, XPT-USD, XPD-USD
 80    - Energy: BRENT
 81
 82    Examples:
 83        >>> import borsapy as bp
 84        >>> usd = bp.FX("USD")
 85        >>> usd.current
 86        {'symbol': 'USD', 'last': 34.85, ...}
 87        >>> usd.history(period="1mo")
 88                         Open    High     Low   Close
 89        Date
 90        2024-12-01    34.50   34.80   34.40   34.75
 91        ...
 92
 93        >>> gold = bp.FX("gram-altin")
 94        >>> gold.current
 95        {'symbol': 'gram-altin', 'last': 2850.50, ...}
 96    """
 97
 98    def __init__(self, asset: str):
 99        """
100        Initialize an FX object.
101
102        Args:
103            asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)
104        """
105        self._asset = asset
106        self._canlidoviz = get_canlidoviz_provider()
107        self._dovizcom = get_dovizcom_provider()
108        self._tradingview = get_tradingview_provider()
109        self._current_cache: dict[str, Any] | None = None
110
111    def _use_canlidoviz(self) -> bool:
112        """Check if canlidoviz should be used for this asset."""
113        asset_upper = self._asset.upper()
114        # Currencies
115        if asset_upper in self._canlidoviz.CURRENCY_IDS:
116            return True
117        # Metals supported by canlidoviz (TRY prices)
118        if self._asset in self._canlidoviz.METAL_IDS:
119            return True
120        # Energy supported by canlidoviz (USD prices)
121        if asset_upper in self._canlidoviz.ENERGY_IDS:
122            return True
123        # Commodities supported by canlidoviz (USD prices)
124        if asset_upper in self._canlidoviz.COMMODITY_IDS:
125            return True
126        return False
127
128    def _get_tradingview_symbol(self) -> tuple[str, str] | None:
129        """Get TradingView exchange and symbol for this asset.
130
131        Returns:
132            Tuple of (exchange, symbol) or None if not supported.
133        """
134        asset_upper = self._asset.upper()
135
136        # Check currency map first
137        if asset_upper in TV_CURRENCY_MAP:
138            return TV_CURRENCY_MAP[asset_upper]
139
140        # Check commodity map
141        if self._asset in TV_COMMODITY_MAP:
142            return TV_COMMODITY_MAP[self._asset]
143        if asset_upper in TV_COMMODITY_MAP:
144            return TV_COMMODITY_MAP[asset_upper]
145
146        return None
147
148    @property
149    def asset(self) -> str:
150        """Return the asset code."""
151        return self._asset
152
153    @property
154    def symbol(self) -> str:
155        """Return the asset code (alias for asset)."""
156        return self._asset
157
158    @property
159    def current(self) -> dict[str, Any]:
160        """
161        Get current price information.
162
163        Returns:
164            Dictionary with current market data:
165            - symbol: Asset code
166            - last: Last price
167            - open: Opening price
168            - high: Day high
169            - low: Day low
170            - update_time: Last update timestamp
171        """
172        if self._current_cache is None:
173            if self._use_canlidoviz():
174                self._current_cache = self._canlidoviz.get_current(self._asset)
175            else:
176                try:
177                    self._current_cache = self._dovizcom.get_current(self._asset)
178                except Exception:
179                    # Fallback to bank_rates for currencies not supported by APIs
180                    self._current_cache = self._current_from_bank_rates()
181        return self._current_cache
182
183    def _current_from_bank_rates(self) -> dict[str, Any]:
184        """Calculate current price from bank rates as fallback."""
185        from datetime import datetime
186
187        rates = self._dovizcom.get_bank_rates(self._asset)
188        if rates.empty:
189            raise ValueError(f"No data available for {self._asset}")
190
191        # Calculate average mid price from all banks
192        mids = (rates["buy"] + rates["sell"]) / 2
193        avg_mid = float(mids.mean())
194
195        return {
196            "symbol": self._asset,
197            "last": avg_mid,
198            "open": avg_mid,
199            "high": float(rates["sell"].max()),
200            "low": float(rates["buy"].min()),
201            "update_time": datetime.now(),
202            "source": "bank_rates_avg",
203        }
204
205    @property
206    def info(self) -> dict[str, Any]:
207        """Alias for current property (yfinance compatibility)."""
208        return self.current
209
210    @property
211    def bank_rates(self) -> pd.DataFrame:
212        """
213        Get exchange rates from all banks.
214
215        Returns:
216            DataFrame with columns: bank, bank_name, currency, buy, sell, spread
217
218        Examples:
219            >>> usd = FX("USD")
220            >>> usd.bank_rates
221                      bank        bank_name currency      buy     sell  spread
222            0       akbank           Akbank      USD  41.6610  44.1610    5.99
223            1      garanti     Garanti BBVA      USD  41.7000  44.2000    5.99
224            ...
225        """
226        return self._dovizcom.get_bank_rates(self._asset)
227
228    def bank_rate(self, bank: str) -> dict[str, Any]:
229        """
230        Get exchange rate from a specific bank.
231
232        Args:
233            bank: Bank code (akbank, garanti, isbank, ziraat, etc.)
234
235        Returns:
236            Dictionary with keys: bank, currency, buy, sell, spread
237
238        Examples:
239            >>> usd = FX("USD")
240            >>> usd.bank_rate("akbank")
241            {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99}
242        """
243        return self._dovizcom.get_bank_rates(self._asset, bank=bank)
244
245    @staticmethod
246    def banks() -> list[str]:
247        """
248        Get list of supported banks.
249
250        Returns:
251            List of bank codes.
252
253        Examples:
254            >>> FX.banks()
255            ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
256        """
257        from borsapy._providers.dovizcom import get_dovizcom_provider
258
259        return get_dovizcom_provider().get_banks()
260
261    @property
262    def institution_rates(self) -> pd.DataFrame:
263        """
264        Get precious metal rates from all institutions (kuyumcular, bankalar).
265
266        Only available for precious metals: gram-altin, gram-gumus, ons-altin,
267        gram-platin
268
269        Returns:
270            DataFrame with columns: institution, institution_name, asset, buy, sell, spread
271
272        Examples:
273            >>> gold = FX("gram-altin")
274            >>> gold.institution_rates
275                   institution  institution_name       asset      buy     sell  spread
276            0      altinkaynak       Altınkaynak  gram-altin  6315.00  6340.00    0.40
277            1           akbank            Akbank  gram-altin  6310.00  6330.00    0.32
278            ...
279        """
280        return self._dovizcom.get_metal_institution_rates(self._asset)
281
282    def institution_rate(self, institution: str) -> dict[str, Any]:
283        """
284        Get precious metal rate from a specific institution.
285
286        Args:
287            institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.)
288
289        Returns:
290            Dictionary with keys: institution, institution_name, asset, buy, sell, spread
291
292        Examples:
293            >>> gold = FX("gram-altin")
294            >>> gold.institution_rate("akbank")
295            {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin',
296             'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32}
297        """
298        return self._dovizcom.get_metal_institution_rates(self._asset, institution=institution)
299
300    @staticmethod
301    def metal_institutions() -> list[str]:
302        """
303        Get list of supported precious metal assets for institution rates.
304
305        Returns:
306            List of asset codes that support institution_rates.
307
308        Examples:
309            >>> FX.metal_institutions()
310            ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']
311        """
312        from borsapy._providers.dovizcom import get_dovizcom_provider
313
314        return get_dovizcom_provider().get_metal_institutions()
315
316    def history(
317        self,
318        period: str = "1mo",
319        interval: str = "1d",
320        start: datetime | str | None = None,
321        end: datetime | str | None = None,
322    ) -> pd.DataFrame:
323        """
324        Get historical OHLC data.
325
326        Args:
327            period: How much data to fetch. Valid periods:
328                    1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max.
329                    Ignored if start is provided.
330            interval: Data interval. Valid intervals:
331                    1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo.
332                    Note: Intraday intervals (1m-4h) use TradingView.
333                    Daily and above use canlidoviz/dovizcom.
334            start: Start date (string or datetime).
335            end: End date (string or datetime). Defaults to today.
336
337        Returns:
338            DataFrame with columns: Open, High, Low, Close, Volume.
339            Index is the Date.
340
341        Examples:
342            >>> fx = FX("USD")
343            >>> fx.history(period="1mo")  # Last month daily
344            >>> fx.history(period="1d", interval="1m")  # Today's minute data
345            >>> fx.history(period="5d", interval="1h")  # 5 days hourly
346            >>> fx.history(start="2024-01-01", end="2024-06-30")  # Date range
347        """
348        start_dt = self._parse_date(start) if start else None
349        end_dt = self._parse_date(end) if end else None
350
351        # Use TradingView for intraday intervals
352        intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h")
353        if interval in intraday_intervals:
354            tv_info = self._get_tradingview_symbol()
355            if tv_info is None:
356                raise ValueError(
357                    f"Intraday data not available for {self._asset}. "
358                    f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}"
359                )
360
361            exchange, symbol = tv_info
362            return self._tradingview.get_history(
363                symbol=symbol,
364                period=period,
365                interval=interval,
366                start=start_dt,
367                end=end_dt,
368                exchange=exchange,
369            )
370
371        # Use canlidoviz/dovizcom for daily and above
372        if self._use_canlidoviz():
373            return self._canlidoviz.get_history(
374                asset=self._asset,
375                period=period,
376                start=start_dt,
377                end=end_dt,
378            )
379        else:
380            return self._dovizcom.get_history(
381                asset=self._asset,
382                period=period,
383                start=start_dt,
384                end=end_dt,
385            )
386
387    def institution_history(
388        self,
389        institution: str,
390        period: str = "1mo",
391        start: datetime | str | None = None,
392        end: datetime | str | None = None,
393    ) -> pd.DataFrame:
394        """
395        Get historical OHLC data from a specific institution.
396
397        Supports both precious metals and currencies.
398
399        Args:
400            institution: Institution slug (akbank, kapalicarsi, harem, etc.)
401            period: How much data to fetch. Valid periods:
402                    1d, 5d, 1mo, 3mo, 6mo, 1y.
403                    Ignored if start is provided.
404            start: Start date (string or datetime).
405            end: End date (string or datetime). Defaults to today.
406
407        Returns:
408            DataFrame with columns: Open, High, Low, Close.
409            Index is the Date.
410            Note: Banks typically return only Close values (Open/High/Low = 0).
411
412        Examples:
413            >>> # Metal history
414            >>> gold = FX("gram-altin")
415            >>> gold.institution_history("akbank", period="1mo")
416            >>> gold.institution_history("kapalicarsi", start="2024-01-01")
417
418            >>> # Currency history
419            >>> usd = FX("USD")
420            >>> usd.institution_history("akbank", period="1mo")
421            >>> usd.institution_history("garanti-bbva", period="5d")
422        """
423        start_dt = self._parse_date(start) if start else None
424        end_dt = self._parse_date(end) if end else None
425
426        # Use canlidoviz for currencies and precious metals (bank-specific rates)
427        asset_upper = self._asset.upper()
428        use_canlidoviz = (
429            asset_upper in self._canlidoviz.CURRENCY_IDS
430            or self._asset in ("gram-altin", "gumus", "gram-platin")
431        )
432
433        if use_canlidoviz:
434            # Check if canlidoviz has bank ID for this asset
435            try:
436                return self._canlidoviz.get_history(
437                    asset=self._asset,
438                    period=period,
439                    start=start_dt,
440                    end=end_dt,
441                    institution=institution,
442                )
443            except Exception:
444                # Fall back to dovizcom if canlidoviz doesn't support this bank
445                pass
446
447        # Use dovizcom for other metals and unsupported banks
448        return self._dovizcom.get_institution_history(
449            asset=self._asset,
450            institution=institution,
451            period=period,
452            start=start_dt,
453            end=end_dt,
454        )
455
456    def _parse_date(self, date: str | datetime) -> datetime:
457        """Parse a date string to datetime."""
458        if isinstance(date, datetime):
459            return date
460        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
461            try:
462                return datetime.strptime(date, fmt)
463            except ValueError:
464                continue
465        raise ValueError(f"Could not parse date: {date}")
466
467    def _get_ta_symbol_info(self) -> tuple[str, str]:
468        """Get TradingView symbol and screener for TA signals.
469
470        Returns:
471            Tuple of (tv_symbol, screener) for TradingView Scanner API.
472
473        Raises:
474            NotImplementedError: If TA signals not available for this asset.
475        """
476        tv_info = self._get_tradingview_symbol()
477        if tv_info is None:
478            raise NotImplementedError(
479                f"TA signals not available for {self._asset}. "
480                f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}. "
481                f"Supported commodities: {list(TV_COMMODITY_MAP.keys())}."
482            )
483        exchange, symbol = tv_info
484        return (f"{exchange}:{symbol}", "forex")
485
486    def __repr__(self) -> str:
487        return f"FX('{self._asset}')"

A yfinance-like interface for forex and commodity data.

Supported assets:

  • Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD (+ 58 more via canlidoviz)
  • Precious Metals: gram-altin, gumus, ons-altin, gram-platin, XAG-USD, XPT-USD, XPD-USD
  • Energy: BRENT

Examples:

import borsapy as bp usd = bp.FX("USD") usd.current {'symbol': 'USD', 'last': 34.85, ...} usd.history(period="1mo") Open High Low Close Date 2024-12-01 34.50 34.80 34.40 34.75 ...

>>> gold = bp.FX("gram-altin")
>>> gold.current
{'symbol': 'gram-altin', 'last': 2850.50, ...}
FX(asset: str)
 98    def __init__(self, asset: str):
 99        """
100        Initialize an FX object.
101
102        Args:
103            asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)
104        """
105        self._asset = asset
106        self._canlidoviz = get_canlidoviz_provider()
107        self._dovizcom = get_dovizcom_provider()
108        self._tradingview = get_tradingview_provider()
109        self._current_cache: dict[str, Any] | None = None

Initialize an FX object.

Args: asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)

asset: str
148    @property
149    def asset(self) -> str:
150        """Return the asset code."""
151        return self._asset

Return the asset code.

symbol: str
153    @property
154    def symbol(self) -> str:
155        """Return the asset code (alias for asset)."""
156        return self._asset

Return the asset code (alias for asset).

current: dict[str, typing.Any]
158    @property
159    def current(self) -> dict[str, Any]:
160        """
161        Get current price information.
162
163        Returns:
164            Dictionary with current market data:
165            - symbol: Asset code
166            - last: Last price
167            - open: Opening price
168            - high: Day high
169            - low: Day low
170            - update_time: Last update timestamp
171        """
172        if self._current_cache is None:
173            if self._use_canlidoviz():
174                self._current_cache = self._canlidoviz.get_current(self._asset)
175            else:
176                try:
177                    self._current_cache = self._dovizcom.get_current(self._asset)
178                except Exception:
179                    # Fallback to bank_rates for currencies not supported by APIs
180                    self._current_cache = self._current_from_bank_rates()
181        return self._current_cache

Get current price information.

Returns: Dictionary with current market data: - symbol: Asset code - last: Last price - open: Opening price - high: Day high - low: Day low - update_time: Last update timestamp

info: dict[str, typing.Any]
205    @property
206    def info(self) -> dict[str, Any]:
207        """Alias for current property (yfinance compatibility)."""
208        return self.current

Alias for current property (yfinance compatibility).

bank_rates: pandas.core.frame.DataFrame
210    @property
211    def bank_rates(self) -> pd.DataFrame:
212        """
213        Get exchange rates from all banks.
214
215        Returns:
216            DataFrame with columns: bank, bank_name, currency, buy, sell, spread
217
218        Examples:
219            >>> usd = FX("USD")
220            >>> usd.bank_rates
221                      bank        bank_name currency      buy     sell  spread
222            0       akbank           Akbank      USD  41.6610  44.1610    5.99
223            1      garanti     Garanti BBVA      USD  41.7000  44.2000    5.99
224            ...
225        """
226        return self._dovizcom.get_bank_rates(self._asset)

Get exchange rates from all banks.

Returns: DataFrame with columns: bank, bank_name, currency, buy, sell, spread

Examples:

usd = FX("USD") usd.bank_rates bank bank_name currency buy sell spread 0 akbank Akbank USD 41.6610 44.1610 5.99 1 garanti Garanti BBVA USD 41.7000 44.2000 5.99 ...

def bank_rate(self, bank: str) -> dict[str, typing.Any]:
228    def bank_rate(self, bank: str) -> dict[str, Any]:
229        """
230        Get exchange rate from a specific bank.
231
232        Args:
233            bank: Bank code (akbank, garanti, isbank, ziraat, etc.)
234
235        Returns:
236            Dictionary with keys: bank, currency, buy, sell, spread
237
238        Examples:
239            >>> usd = FX("USD")
240            >>> usd.bank_rate("akbank")
241            {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99}
242        """
243        return self._dovizcom.get_bank_rates(self._asset, bank=bank)

Get exchange rate from a specific bank.

Args: bank: Bank code (akbank, garanti, isbank, ziraat, etc.)

Returns: Dictionary with keys: bank, currency, buy, sell, spread

Examples:

usd = FX("USD") usd.bank_rate("akbank") {'bank': 'akbank', 'currency': 'USD', 'buy': 41.6610, 'sell': 44.1610, 'spread': 5.99}

@staticmethod
def banks() -> list[str]:
245    @staticmethod
246    def banks() -> list[str]:
247        """
248        Get list of supported banks.
249
250        Returns:
251            List of bank codes.
252
253        Examples:
254            >>> FX.banks()
255            ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
256        """
257        from borsapy._providers.dovizcom import get_dovizcom_provider
258
259        return get_dovizcom_provider().get_banks()

Get list of supported banks.

Returns: List of bank codes.

Examples:

FX.banks() ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]

institution_rates: pandas.core.frame.DataFrame
261    @property
262    def institution_rates(self) -> pd.DataFrame:
263        """
264        Get precious metal rates from all institutions (kuyumcular, bankalar).
265
266        Only available for precious metals: gram-altin, gram-gumus, ons-altin,
267        gram-platin
268
269        Returns:
270            DataFrame with columns: institution, institution_name, asset, buy, sell, spread
271
272        Examples:
273            >>> gold = FX("gram-altin")
274            >>> gold.institution_rates
275                   institution  institution_name       asset      buy     sell  spread
276            0      altinkaynak       Altınkaynak  gram-altin  6315.00  6340.00    0.40
277            1           akbank            Akbank  gram-altin  6310.00  6330.00    0.32
278            ...
279        """
280        return self._dovizcom.get_metal_institution_rates(self._asset)

Get precious metal rates from all institutions (kuyumcular, bankalar).

Only available for precious metals: gram-altin, gram-gumus, ons-altin, gram-platin

Returns: DataFrame with columns: institution, institution_name, asset, buy, sell, spread

Examples:

gold = FX("gram-altin") gold.institution_rates institution institution_name asset buy sell spread 0 altinkaynak Altınkaynak gram-altin 6315.00 6340.00 0.40 1 akbank Akbank gram-altin 6310.00 6330.00 0.32 ...

def institution_rate(self, institution: str) -> dict[str, typing.Any]:
282    def institution_rate(self, institution: str) -> dict[str, Any]:
283        """
284        Get precious metal rate from a specific institution.
285
286        Args:
287            institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.)
288
289        Returns:
290            Dictionary with keys: institution, institution_name, asset, buy, sell, spread
291
292        Examples:
293            >>> gold = FX("gram-altin")
294            >>> gold.institution_rate("akbank")
295            {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin',
296             'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32}
297        """
298        return self._dovizcom.get_metal_institution_rates(self._asset, institution=institution)

Get precious metal rate from a specific institution.

Args: institution: Institution slug (kapalicarsi, altinkaynak, akbank, etc.)

Returns: Dictionary with keys: institution, institution_name, asset, buy, sell, spread

Examples:

gold = FX("gram-altin") gold.institution_rate("akbank") {'institution': 'akbank', 'institution_name': 'Akbank', 'asset': 'gram-altin', 'buy': 6310.00, 'sell': 6330.00, 'spread': 0.32}

@staticmethod
def metal_institutions() -> list[str]:
300    @staticmethod
301    def metal_institutions() -> list[str]:
302        """
303        Get list of supported precious metal assets for institution rates.
304
305        Returns:
306            List of asset codes that support institution_rates.
307
308        Examples:
309            >>> FX.metal_institutions()
310            ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']
311        """
312        from borsapy._providers.dovizcom import get_dovizcom_provider
313
314        return get_dovizcom_provider().get_metal_institutions()

Get list of supported precious metal assets for institution rates.

Returns: List of asset codes that support institution_rates.

Examples:

FX.metal_institutions() ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
316    def history(
317        self,
318        period: str = "1mo",
319        interval: str = "1d",
320        start: datetime | str | None = None,
321        end: datetime | str | None = None,
322    ) -> pd.DataFrame:
323        """
324        Get historical OHLC data.
325
326        Args:
327            period: How much data to fetch. Valid periods:
328                    1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max.
329                    Ignored if start is provided.
330            interval: Data interval. Valid intervals:
331                    1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo.
332                    Note: Intraday intervals (1m-4h) use TradingView.
333                    Daily and above use canlidoviz/dovizcom.
334            start: Start date (string or datetime).
335            end: End date (string or datetime). Defaults to today.
336
337        Returns:
338            DataFrame with columns: Open, High, Low, Close, Volume.
339            Index is the Date.
340
341        Examples:
342            >>> fx = FX("USD")
343            >>> fx.history(period="1mo")  # Last month daily
344            >>> fx.history(period="1d", interval="1m")  # Today's minute data
345            >>> fx.history(period="5d", interval="1h")  # 5 days hourly
346            >>> fx.history(start="2024-01-01", end="2024-06-30")  # Date range
347        """
348        start_dt = self._parse_date(start) if start else None
349        end_dt = self._parse_date(end) if end else None
350
351        # Use TradingView for intraday intervals
352        intraday_intervals = ("1m", "5m", "15m", "30m", "1h", "4h")
353        if interval in intraday_intervals:
354            tv_info = self._get_tradingview_symbol()
355            if tv_info is None:
356                raise ValueError(
357                    f"Intraday data not available for {self._asset}. "
358                    f"Supported currencies: {list(TV_CURRENCY_MAP.keys())}"
359                )
360
361            exchange, symbol = tv_info
362            return self._tradingview.get_history(
363                symbol=symbol,
364                period=period,
365                interval=interval,
366                start=start_dt,
367                end=end_dt,
368                exchange=exchange,
369            )
370
371        # Use canlidoviz/dovizcom for daily and above
372        if self._use_canlidoviz():
373            return self._canlidoviz.get_history(
374                asset=self._asset,
375                period=period,
376                start=start_dt,
377                end=end_dt,
378            )
379        else:
380            return self._dovizcom.get_history(
381                asset=self._asset,
382                period=period,
383                start=start_dt,
384                end=end_dt,
385            )

Get historical OHLC data.

Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. Ignored if start is provided. interval: Data interval. Valid intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk, 1mo. Note: Intraday intervals (1m-4h) use TradingView. Daily and above use canlidoviz/dovizcom. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.

Returns: DataFrame with columns: Open, High, Low, Close, Volume. Index is the Date.

Examples:

fx = FX("USD") fx.history(period="1mo") # Last month daily fx.history(period="1d", interval="1m") # Today's minute data fx.history(period="5d", interval="1h") # 5 days hourly fx.history(start="2024-01-01", end="2024-06-30") # Date range

def institution_history( self, institution: str, period: str = '1mo', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
387    def institution_history(
388        self,
389        institution: str,
390        period: str = "1mo",
391        start: datetime | str | None = None,
392        end: datetime | str | None = None,
393    ) -> pd.DataFrame:
394        """
395        Get historical OHLC data from a specific institution.
396
397        Supports both precious metals and currencies.
398
399        Args:
400            institution: Institution slug (akbank, kapalicarsi, harem, etc.)
401            period: How much data to fetch. Valid periods:
402                    1d, 5d, 1mo, 3mo, 6mo, 1y.
403                    Ignored if start is provided.
404            start: Start date (string or datetime).
405            end: End date (string or datetime). Defaults to today.
406
407        Returns:
408            DataFrame with columns: Open, High, Low, Close.
409            Index is the Date.
410            Note: Banks typically return only Close values (Open/High/Low = 0).
411
412        Examples:
413            >>> # Metal history
414            >>> gold = FX("gram-altin")
415            >>> gold.institution_history("akbank", period="1mo")
416            >>> gold.institution_history("kapalicarsi", start="2024-01-01")
417
418            >>> # Currency history
419            >>> usd = FX("USD")
420            >>> usd.institution_history("akbank", period="1mo")
421            >>> usd.institution_history("garanti-bbva", period="5d")
422        """
423        start_dt = self._parse_date(start) if start else None
424        end_dt = self._parse_date(end) if end else None
425
426        # Use canlidoviz for currencies and precious metals (bank-specific rates)
427        asset_upper = self._asset.upper()
428        use_canlidoviz = (
429            asset_upper in self._canlidoviz.CURRENCY_IDS
430            or self._asset in ("gram-altin", "gumus", "gram-platin")
431        )
432
433        if use_canlidoviz:
434            # Check if canlidoviz has bank ID for this asset
435            try:
436                return self._canlidoviz.get_history(
437                    asset=self._asset,
438                    period=period,
439                    start=start_dt,
440                    end=end_dt,
441                    institution=institution,
442                )
443            except Exception:
444                # Fall back to dovizcom if canlidoviz doesn't support this bank
445                pass
446
447        # Use dovizcom for other metals and unsupported banks
448        return self._dovizcom.get_institution_history(
449            asset=self._asset,
450            institution=institution,
451            period=period,
452            start=start_dt,
453            end=end_dt,
454        )

Get historical OHLC data from a specific institution.

Supports both precious metals and currencies.

Args: institution: Institution slug (akbank, kapalicarsi, harem, etc.) period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.

Returns: DataFrame with columns: Open, High, Low, Close. Index is the Date. Note: Banks typically return only Close values (Open/High/Low = 0).

Examples:

Metal history

gold = FX("gram-altin") gold.institution_history("akbank", period="1mo") gold.institution_history("kapalicarsi", start="2024-01-01")

>>> # Currency history
>>> usd = FX("USD")
>>> usd.institution_history("akbank", period="1mo")
>>> usd.institution_history("garanti-bbva", period="5d")
class Crypto(borsapy.technical.TechnicalMixin):
 13class Crypto(TechnicalMixin):
 14    """
 15    A yfinance-like interface for cryptocurrency data from BtcTurk.
 16
 17    Examples:
 18        >>> import borsapy as bp
 19        >>> btc = bp.Crypto("BTCTRY")
 20        >>> btc.current
 21        {'symbol': 'BTCTRY', 'last': 3500000.0, ...}
 22        >>> btc.history(period="1mo")
 23                             Open       High        Low      Close      Volume
 24        Date
 25        2024-12-01   3400000.0  3550000.0  3380000.0  3500000.0   1234.5678
 26        ...
 27
 28        >>> eth = bp.Crypto("ETHTRY")
 29        >>> eth.current['last']
 30        125000.0
 31    """
 32
 33    def __init__(self, pair: str):
 34        """
 35        Initialize a Crypto object.
 36
 37        Args:
 38            pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT").
 39                  Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY
 40        """
 41        self._pair = pair.upper()
 42        self._provider = get_btcturk_provider()
 43        self._current_cache: dict[str, Any] | None = None
 44
 45    @property
 46    def pair(self) -> str:
 47        """Return the trading pair."""
 48        return self._pair
 49
 50    @property
 51    def symbol(self) -> str:
 52        """Return the trading pair (alias)."""
 53        return self._pair
 54
 55    @property
 56    def current(self) -> dict[str, Any]:
 57        """
 58        Get current ticker information.
 59
 60        Returns:
 61            Dictionary with current market data:
 62            - symbol: Trading pair
 63            - last: Last traded price
 64            - open: Opening price
 65            - high: 24h high
 66            - low: 24h low
 67            - bid: Best bid price
 68            - ask: Best ask price
 69            - volume: 24h volume
 70            - change: Price change
 71            - change_percent: Percent change
 72        """
 73        if self._current_cache is None:
 74            self._current_cache = self._provider.get_ticker(self._pair)
 75        return self._current_cache
 76
 77    @property
 78    def info(self) -> dict[str, Any]:
 79        """Alias for current property (yfinance compatibility)."""
 80        return self.current
 81
 82    def history(
 83        self,
 84        period: str = "1mo",
 85        interval: str = "1d",
 86        start: datetime | str | None = None,
 87        end: datetime | str | None = None,
 88    ) -> pd.DataFrame:
 89        """
 90        Get historical OHLCV data.
 91
 92        Args:
 93            period: How much data to fetch. Valid periods:
 94                    1d, 5d, 1mo, 3mo, 6mo, 1y.
 95                    Ignored if start is provided.
 96            interval: Data granularity. Valid intervals:
 97                      1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk.
 98            start: Start date (string or datetime).
 99            end: End date (string or datetime). Defaults to now.
100
101        Returns:
102            DataFrame with columns: Open, High, Low, Close, Volume.
103            Index is the Date.
104
105        Examples:
106            >>> crypto = Crypto("BTCTRY")
107            >>> crypto.history(period="1mo")  # Last month
108            >>> crypto.history(period="1y", interval="1wk")  # Weekly for 1 year
109            >>> crypto.history(start="2024-01-01", end="2024-06-30")  # Date range
110        """
111        start_dt = self._parse_date(start) if start else None
112        end_dt = self._parse_date(end) if end else None
113
114        return self._provider.get_history(
115            pair=self._pair,
116            period=period,
117            interval=interval,
118            start=start_dt,
119            end=end_dt,
120        )
121
122    def _parse_date(self, date: str | datetime) -> datetime:
123        """Parse a date string to datetime."""
124        if isinstance(date, datetime):
125            return date
126        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
127            try:
128                return datetime.strptime(date, fmt)
129            except ValueError:
130                continue
131        raise ValueError(f"Could not parse date: {date}")
132
133    def _get_ta_symbol_info(self) -> tuple[str, str]:
134        """Get TradingView symbol and screener for TA signals.
135
136        Maps BtcTurk pairs to Binance USDT pairs for better TradingView coverage.
137
138        Returns:
139            Tuple of (tv_symbol, screener) for TradingView Scanner API.
140        """
141        # Extract base currency from pair (e.g., "BTCTRY" -> "BTC")
142        base = self._pair.replace("TRY", "").replace("USDT", "")
143        # Use Binance USDT pair for better TradingView coverage
144        return (f"BINANCE:{base}USDT", "crypto")
145
146    def __repr__(self) -> str:
147        return f"Crypto('{self._pair}')"

A yfinance-like interface for cryptocurrency data from BtcTurk.

Examples:

import borsapy as bp btc = bp.Crypto("BTCTRY") btc.current {'symbol': 'BTCTRY', 'last': 3500000.0, ...} btc.history(period="1mo") Open High Low Close Volume Date 2024-12-01 3400000.0 3550000.0 3380000.0 3500000.0 1234.5678 ...

>>> eth = bp.Crypto("ETHTRY")
>>> eth.current['last']
125000.0
Crypto(pair: str)
33    def __init__(self, pair: str):
34        """
35        Initialize a Crypto object.
36
37        Args:
38            pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT").
39                  Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY
40        """
41        self._pair = pair.upper()
42        self._provider = get_btcturk_provider()
43        self._current_cache: dict[str, Any] | None = None

Initialize a Crypto object.

Args: pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT"). Common pairs: BTCTRY, ETHTRY, XRPTRY, DOGETRY, SOLTRY

pair: str
45    @property
46    def pair(self) -> str:
47        """Return the trading pair."""
48        return self._pair

Return the trading pair.

symbol: str
50    @property
51    def symbol(self) -> str:
52        """Return the trading pair (alias)."""
53        return self._pair

Return the trading pair (alias).

current: dict[str, typing.Any]
55    @property
56    def current(self) -> dict[str, Any]:
57        """
58        Get current ticker information.
59
60        Returns:
61            Dictionary with current market data:
62            - symbol: Trading pair
63            - last: Last traded price
64            - open: Opening price
65            - high: 24h high
66            - low: 24h low
67            - bid: Best bid price
68            - ask: Best ask price
69            - volume: 24h volume
70            - change: Price change
71            - change_percent: Percent change
72        """
73        if self._current_cache is None:
74            self._current_cache = self._provider.get_ticker(self._pair)
75        return self._current_cache

Get current ticker information.

Returns: Dictionary with current market data: - symbol: Trading pair - last: Last traded price - open: Opening price - high: 24h high - low: 24h low - bid: Best bid price - ask: Best ask price - volume: 24h volume - change: Price change - change_percent: Percent change

info: dict[str, typing.Any]
77    @property
78    def info(self) -> dict[str, Any]:
79        """Alias for current property (yfinance compatibility)."""
80        return self.current

Alias for current property (yfinance compatibility).

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
 82    def history(
 83        self,
 84        period: str = "1mo",
 85        interval: str = "1d",
 86        start: datetime | str | None = None,
 87        end: datetime | str | None = None,
 88    ) -> pd.DataFrame:
 89        """
 90        Get historical OHLCV data.
 91
 92        Args:
 93            period: How much data to fetch. Valid periods:
 94                    1d, 5d, 1mo, 3mo, 6mo, 1y.
 95                    Ignored if start is provided.
 96            interval: Data granularity. Valid intervals:
 97                      1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk.
 98            start: Start date (string or datetime).
 99            end: End date (string or datetime). Defaults to now.
100
101        Returns:
102            DataFrame with columns: Open, High, Low, Close, Volume.
103            Index is the Date.
104
105        Examples:
106            >>> crypto = Crypto("BTCTRY")
107            >>> crypto.history(period="1mo")  # Last month
108            >>> crypto.history(period="1y", interval="1wk")  # Weekly for 1 year
109            >>> crypto.history(start="2024-01-01", end="2024-06-30")  # Date range
110        """
111        start_dt = self._parse_date(start) if start else None
112        end_dt = self._parse_date(end) if end else None
113
114        return self._provider.get_history(
115            pair=self._pair,
116            period=period,
117            interval=interval,
118            start=start_dt,
119            end=end_dt,
120        )

Get historical OHLCV data.

Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. interval: Data granularity. Valid intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk. start: Start date (string or datetime). end: End date (string or datetime). Defaults to now.

Returns: DataFrame with columns: Open, High, Low, Close, Volume. Index is the Date.

Examples:

crypto = Crypto("BTCTRY") crypto.history(period="1mo") # Last month crypto.history(period="1y", interval="1wk") # Weekly for 1 year crypto.history(start="2024-01-01", end="2024-06-30") # Date range

class Fund(borsapy.technical.TechnicalMixin):
 15class Fund(TechnicalMixin):
 16    """
 17    A yfinance-like interface for mutual fund data from TEFAS.
 18
 19    Examples:
 20        >>> import borsapy as bp
 21        >>> fund = bp.Fund("AAK")
 22        >>> fund.info
 23        {'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...}
 24        >>> fund.history(period="1mo")
 25                         Price      FundSize  Investors
 26        Date
 27        2024-12-01      1.200  150000000.0       5000
 28        ...
 29
 30        >>> fund = bp.Fund("TTE")
 31        >>> fund.info['return_1y']
 32        45.67
 33    """
 34
 35    def __init__(self, fund_code: str, fund_type: str | None = None):
 36        """
 37        Initialize a Fund object.
 38
 39        Args:
 40            fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF")
 41            fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds.
 42                      If None, auto-detects by trying YAT first, then EMK.
 43
 44        Examples:
 45            >>> fund = bp.Fund("AAK")              # Investment fund (auto-detect)
 46            >>> fund = bp.Fund("HEF", fund_type="EMK")  # Pension fund (explicit)
 47        """
 48        self._fund_code = fund_code.upper()
 49        self._fund_type = fund_type.upper() if fund_type else None
 50        self._provider = get_tefas_provider()
 51        self._info_cache: dict[str, Any] | None = None
 52        self._detected_fund_type: str | None = None
 53
 54    @property
 55    def fund_code(self) -> str:
 56        """Return the fund code."""
 57        return self._fund_code
 58
 59    @property
 60    def symbol(self) -> str:
 61        """Return the fund code (alias)."""
 62        return self._fund_code
 63
 64    @property
 65    def fund_type(self) -> str:
 66        """
 67        Return the fund type ("YAT" or "EMK").
 68
 69        If not explicitly set, auto-detects on first history() or allocation() call.
 70        """
 71        if self._fund_type:
 72            return self._fund_type
 73        if self._detected_fund_type:
 74            return self._detected_fund_type
 75
 76        # Auto-detect by trying history with YAT first, then EMK
 77        self._detect_fund_type()
 78        return self._detected_fund_type or "YAT"
 79
 80    def _detect_fund_type(self) -> None:
 81        """Auto-detect fund type by trying history API with different fund types."""
 82        if self._fund_type or self._detected_fund_type:
 83            return
 84
 85        from datetime import timedelta
 86
 87        end_dt = datetime.now()
 88        start_dt = end_dt - timedelta(days=7)
 89
 90        # Try YAT first
 91        try:
 92            df = self._provider._fetch_history_chunk(
 93                self._fund_code, start_dt, end_dt, fund_type="YAT"
 94            )
 95            if not df.empty:
 96                self._detected_fund_type = "YAT"
 97                return
 98        except DataNotAvailableError:
 99            pass
100
101        # Try EMK
102        try:
103            df = self._provider._fetch_history_chunk(
104                self._fund_code, start_dt, end_dt, fund_type="EMK"
105            )
106            if not df.empty:
107                self._detected_fund_type = "EMK"
108                return
109        except DataNotAvailableError:
110            pass
111
112        # Default to YAT if neither works
113        self._detected_fund_type = "YAT"
114
115    @property
116    def info(self) -> dict[str, Any]:
117        """
118        Get detailed fund information.
119
120        Returns:
121            Dictionary with fund details:
122            - fund_code: TEFAS fund code
123            - name: Fund full name
124            - date: Last update date
125            - price: Current unit price
126            - fund_size: Total fund size (TRY)
127            - investor_count: Number of investors
128            - founder: Fund founder company
129            - manager: Fund manager company
130            - fund_type: Fund type
131            - category: Fund category
132            - risk_value: Risk rating (1-7)
133            - return_1m, return_3m, return_6m: Period returns
134            - return_ytd: Year-to-date return
135            - return_1y, return_3y, return_5y: Annual returns
136            - daily_return: Daily return
137        """
138        if self._info_cache is None:
139            # GetAllFundAnalyzeData works for both YAT and EMK without fontip
140            self._info_cache = self._provider.get_fund_detail(self._fund_code)
141
142            # If fund_type not explicitly set, we need to detect it for history/allocation
143            if not self._fund_type and not self._detected_fund_type:
144                # Detection will happen on first history() call
145                pass
146
147        return self._info_cache
148
149    @property
150    def detail(self) -> dict[str, Any]:
151        """Alias for info property."""
152        return self.info
153
154    @property
155    def performance(self) -> dict[str, Any]:
156        """
157        Get fund performance metrics only.
158
159        Returns:
160            Dictionary with performance data:
161            - daily_return: Daily return
162            - return_1m, return_3m, return_6m: Period returns
163            - return_ytd: Year-to-date return
164            - return_1y, return_3y, return_5y: Annual returns
165        """
166        info = self.info
167        return {
168            "daily_return": info.get("daily_return"),
169            "return_1m": info.get("return_1m"),
170            "return_3m": info.get("return_3m"),
171            "return_6m": info.get("return_6m"),
172            "return_ytd": info.get("return_ytd"),
173            "return_1y": info.get("return_1y"),
174            "return_3y": info.get("return_3y"),
175            "return_5y": info.get("return_5y"),
176        }
177
178    @property
179    def allocation(self) -> pd.DataFrame:
180        """
181        Get current portfolio allocation (asset breakdown) for last 7 days.
182
183        For longer periods, use allocation_history() method.
184
185        Returns:
186            DataFrame with columns: Date, asset_type, asset_name, weight.
187
188        Examples:
189            >>> fund = Fund("AAK")
190            >>> fund.allocation
191                             Date asset_type         asset_name  weight
192            0 2024-12-20         HS        Hisse Senedi   45.32
193            1 2024-12-20         DB        Devlet Bonusu  30.15
194            ...
195        """
196        return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type)
197
198    def allocation_history(
199        self,
200        period: str = "1mo",
201        start: datetime | str | None = None,
202        end: datetime | str | None = None,
203    ) -> pd.DataFrame:
204        """
205        Get historical portfolio allocation (asset breakdown).
206
207        Note: TEFAS API supports maximum ~100 days (3 months) of data.
208
209        Args:
210            period: How much data to fetch. Valid periods:
211                    1d, 5d, 1mo, 3mo (max ~100 days).
212                    Ignored if start is provided.
213            start: Start date (string or datetime).
214            end: End date (string or datetime). Defaults to today.
215
216        Returns:
217            DataFrame with columns: Date, asset_type, asset_name, weight.
218
219        Examples:
220            >>> fund = Fund("AAK")
221            >>> fund.allocation_history(period="1mo")  # Last month
222            >>> fund.allocation_history(period="3mo")  # Last 3 months (max)
223            >>> fund.allocation_history(start="2024-10-01", end="2024-12-31")
224        """
225        start_dt = self._parse_date(start) if start else None
226        end_dt = self._parse_date(end) if end else None
227
228        # If no start date, calculate from period
229        if start_dt is None:
230            from datetime import timedelta
231            end_dt = end_dt or datetime.now()
232            days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30)
233            # Cap at 100 days (API limit)
234            days = min(days, 100)
235            start_dt = end_dt - timedelta(days=days)
236
237        return self._provider.get_allocation(
238            fund_code=self._fund_code,
239            start=start_dt,
240            end=end_dt,
241            fund_type=self.fund_type,
242        )
243
244    def history(
245        self,
246        period: str = "1mo",
247        start: datetime | str | None = None,
248        end: datetime | str | None = None,
249    ) -> pd.DataFrame:
250        """
251        Get historical price data.
252
253        Args:
254            period: How much data to fetch. Valid periods:
255                    1d, 5d, 1mo, 3mo, 6mo, 1y.
256                    Ignored if start is provided.
257            start: Start date (string or datetime).
258            end: End date (string or datetime). Defaults to now.
259
260        Returns:
261            DataFrame with columns: Price, FundSize, Investors.
262            Index is the Date.
263
264        Examples:
265            >>> fund = Fund("AAK")
266            >>> fund.history(period="1mo")  # Last month
267            >>> fund.history(period="1y")  # Last year
268            >>> fund.history(start="2024-01-01", end="2024-06-30")  # Date range
269        """
270        start_dt = self._parse_date(start) if start else None
271        end_dt = self._parse_date(end) if end else None
272
273        return self._provider.get_history(
274            fund_code=self._fund_code,
275            period=period,
276            start=start_dt,
277            end=end_dt,
278            fund_type=self.fund_type,
279        )
280
281    def _parse_date(self, date: str | datetime) -> datetime:
282        """Parse a date string to datetime."""
283        if isinstance(date, datetime):
284            return date
285        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
286            try:
287                return datetime.strptime(date, fmt)
288            except ValueError:
289                continue
290        raise ValueError(f"Could not parse date: {date}")
291
292    def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float:
293        """
294        Calculate the Sharpe ratio for the fund.
295
296        Sharpe Ratio = (Rp - Rf) / σp
297        Where:
298        - Rp = Annualized return of the fund
299        - Rf = Risk-free rate (default: 10Y government bond yield)
300        - σp = Annualized standard deviation of returns
301
302        Args:
303            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
304            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
305                           If None, uses current 10Y bond yield from bp.risk_free_rate().
306
307        Returns:
308            Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).
309
310        Examples:
311            >>> fund = bp.Fund("YAY")
312            >>> fund.sharpe_ratio()  # 1-year Sharpe with current risk-free rate
313            0.85
314
315            >>> fund.sharpe_ratio(period="3y")  # 3-year Sharpe
316            1.23
317
318            >>> fund.sharpe_ratio(risk_free_rate=0.25)  # Custom risk-free rate
319            0.92
320        """
321        metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate)
322        return metrics.get("sharpe_ratio", np.nan)
323
324    def risk_metrics(
325        self,
326        period: str = "1y",
327        risk_free_rate: float | None = None,
328    ) -> dict[str, Any]:
329        """
330        Calculate comprehensive risk metrics for the fund.
331
332        Args:
333            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
334            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
335                           If None, uses current 10Y bond yield.
336
337        Returns:
338            Dictionary with risk metrics:
339            - annualized_return: Annualized return (%)
340            - annualized_volatility: Annualized standard deviation (%)
341            - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp
342            - sortino_ratio: Downside risk-adjusted return
343            - max_drawdown: Maximum peak-to-trough decline (%)
344            - risk_free_rate: Risk-free rate used (%)
345            - trading_days: Number of trading days in the period
346
347        Examples:
348            >>> fund = bp.Fund("YAY")
349            >>> metrics = fund.risk_metrics()
350            >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}")
351            >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")
352        """
353        # Get historical data
354        df = self.history(period=period)
355
356        if df.empty or len(df) < 20:
357            return {
358                "annualized_return": np.nan,
359                "annualized_volatility": np.nan,
360                "sharpe_ratio": np.nan,
361                "sortino_ratio": np.nan,
362                "max_drawdown": np.nan,
363                "risk_free_rate": np.nan,
364                "trading_days": 0,
365            }
366
367        # Calculate daily returns
368        prices = df["Price"]
369        daily_returns = prices.pct_change().dropna()
370        trading_days = len(daily_returns)
371
372        # Annualization factor (trading days per year)
373        annualization_factor = 252
374
375        # Annualized return
376        total_return = (prices.iloc[-1] / prices.iloc[0]) - 1
377        years = trading_days / annualization_factor
378        annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100
379
380        # Annualized volatility
381        daily_volatility = daily_returns.std()
382        annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100
383
384        # Get risk-free rate
385        if risk_free_rate is None:
386            try:
387                from borsapy.bond import risk_free_rate as get_rf_rate
388                rf = get_rf_rate() * 100  # Returns decimal like 0.28, convert to %
389            except Exception:
390                rf = 30.0  # Fallback: approximate Turkish 10Y yield
391        else:
392            rf = risk_free_rate * 100  # Convert decimal to percentage
393
394        # Sharpe Ratio
395        if annualized_volatility > 0:
396            sharpe = (annualized_return - rf) / annualized_volatility
397        else:
398            sharpe = np.nan
399
400        # Sortino Ratio (uses downside deviation)
401        negative_returns = daily_returns[daily_returns < 0]
402        if len(negative_returns) > 0:
403            downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100
404            if downside_deviation > 0:
405                sortino = (annualized_return - rf) / downside_deviation
406            else:
407                sortino = np.nan
408        else:
409            sortino = np.inf  # No negative returns
410
411        # Maximum Drawdown
412        cumulative = (1 + daily_returns).cumprod()
413        running_max = cumulative.cummax()
414        drawdowns = (cumulative - running_max) / running_max
415        max_drawdown = drawdowns.min() * 100  # Negative percentage
416
417        return {
418            "annualized_return": round(annualized_return, 2),
419            "annualized_volatility": round(annualized_volatility, 2),
420            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
421            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
422            "max_drawdown": round(max_drawdown, 2),
423            "risk_free_rate": round(rf, 2),
424            "trading_days": trading_days,
425        }
426
427    def get_holdings(
428        self,
429        api_key: str,
430        period: str | None = None,
431    ) -> pd.DataFrame:
432        """
433        Get detailed portfolio holdings (individual securities).
434
435        Returns the specific stocks, ETFs, and funds held by this fund,
436        with their weights and ISIN codes. Data is sourced from KAP
437        "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures.
438
439        Uses OpenRouter LLM for PDF parsing.
440
441        Args:
442            api_key: OpenRouter API key for LLM parsing.
443                    Get your free API key at: https://openrouter.ai/
444            period: Optional period in format "YYYY-MM" (e.g., "2025-12").
445                   If None, returns the most recent holdings.
446
447        Returns:
448            DataFrame with columns:
449            - symbol: Security symbol (e.g., "GOOGL", "THYAO")
450            - isin: ISIN code
451            - name: Full security name
452            - weight: Portfolio weight (%)
453            - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.)
454            - country: Country ('TR', 'US', or None)
455            - value: Market value in TRY
456
457        Raises:
458            DataNotAvailableError: If holdings data not available.
459            APIError: If LLM parsing fails.
460            ImportError: If required packages are not installed.
461
462        Examples:
463            >>> fund = bp.Fund("YAY")
464            >>> fund.get_holdings(api_key="sk-or-v1-...")
465               symbol              isin                              name  weight   type country         value
466            0   GOOGL  US02079K3059             ALPHABET INC CL A    6.76  stock      US  82478088.0
467            1    AVGO  US11135F1012             BROADCOM INC          5.11  stock      US  62345678.0
468            ...
469
470            >>> # Get holdings for specific period
471            >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12")
472
473            >>> # Filter by type
474            >>> holdings = fund.get_holdings(api_key="sk-or-v1-...")
475            >>> holdings[holdings['type'] == 'stock']
476        """
477        from borsapy._providers.kap_holdings import get_kap_holdings_provider
478
479        provider = get_kap_holdings_provider()
480        return provider.get_holdings_df(self._fund_code, api_key, period=period)
481
482    def __repr__(self) -> str:
483        return f"Fund('{self._fund_code}')"

A yfinance-like interface for mutual fund data from TEFAS.

Examples:

import borsapy as bp fund = bp.Fund("AAK") fund.info {'fund_code': 'AAK', 'name': 'Ak Portföy...', 'price': 1.234, ...} fund.history(period="1mo") Price FundSize Investors Date 2024-12-01 1.200 150000000.0 5000 ...

>>> fund = bp.Fund("TTE")
>>> fund.info['return_1y']
45.67
Fund(fund_code: str, fund_type: str | None = None)
35    def __init__(self, fund_code: str, fund_type: str | None = None):
36        """
37        Initialize a Fund object.
38
39        Args:
40            fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF")
41            fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds.
42                      If None, auto-detects by trying YAT first, then EMK.
43
44        Examples:
45            >>> fund = bp.Fund("AAK")              # Investment fund (auto-detect)
46            >>> fund = bp.Fund("HEF", fund_type="EMK")  # Pension fund (explicit)
47        """
48        self._fund_code = fund_code.upper()
49        self._fund_type = fund_type.upper() if fund_type else None
50        self._provider = get_tefas_provider()
51        self._info_cache: dict[str, Any] | None = None
52        self._detected_fund_type: str | None = None

Initialize a Fund object.

Args: fund_code: TEFAS fund code (e.g., "AAK", "TTE", "YAF") fund_type: Fund type - "YAT" for investment funds, "EMK" for pension funds. If None, auto-detects by trying YAT first, then EMK.

Examples:

fund = bp.Fund("AAK") # Investment fund (auto-detect) fund = bp.Fund("HEF", fund_type="EMK") # Pension fund (explicit)

fund_code: str
54    @property
55    def fund_code(self) -> str:
56        """Return the fund code."""
57        return self._fund_code

Return the fund code.

symbol: str
59    @property
60    def symbol(self) -> str:
61        """Return the fund code (alias)."""
62        return self._fund_code

Return the fund code (alias).

fund_type: str
64    @property
65    def fund_type(self) -> str:
66        """
67        Return the fund type ("YAT" or "EMK").
68
69        If not explicitly set, auto-detects on first history() or allocation() call.
70        """
71        if self._fund_type:
72            return self._fund_type
73        if self._detected_fund_type:
74            return self._detected_fund_type
75
76        # Auto-detect by trying history with YAT first, then EMK
77        self._detect_fund_type()
78        return self._detected_fund_type or "YAT"

Return the fund type ("YAT" or "EMK").

If not explicitly set, auto-detects on first history() or allocation() call.

info: dict[str, typing.Any]
115    @property
116    def info(self) -> dict[str, Any]:
117        """
118        Get detailed fund information.
119
120        Returns:
121            Dictionary with fund details:
122            - fund_code: TEFAS fund code
123            - name: Fund full name
124            - date: Last update date
125            - price: Current unit price
126            - fund_size: Total fund size (TRY)
127            - investor_count: Number of investors
128            - founder: Fund founder company
129            - manager: Fund manager company
130            - fund_type: Fund type
131            - category: Fund category
132            - risk_value: Risk rating (1-7)
133            - return_1m, return_3m, return_6m: Period returns
134            - return_ytd: Year-to-date return
135            - return_1y, return_3y, return_5y: Annual returns
136            - daily_return: Daily return
137        """
138        if self._info_cache is None:
139            # GetAllFundAnalyzeData works for both YAT and EMK without fontip
140            self._info_cache = self._provider.get_fund_detail(self._fund_code)
141
142            # If fund_type not explicitly set, we need to detect it for history/allocation
143            if not self._fund_type and not self._detected_fund_type:
144                # Detection will happen on first history() call
145                pass
146
147        return self._info_cache

Get detailed fund information.

Returns: Dictionary with fund details: - fund_code: TEFAS fund code - name: Fund full name - date: Last update date - price: Current unit price - fund_size: Total fund size (TRY) - investor_count: Number of investors - founder: Fund founder company - manager: Fund manager company - fund_type: Fund type - category: Fund category - risk_value: Risk rating (1-7) - return_1m, return_3m, return_6m: Period returns - return_ytd: Year-to-date return - return_1y, return_3y, return_5y: Annual returns - daily_return: Daily return

detail: dict[str, typing.Any]
149    @property
150    def detail(self) -> dict[str, Any]:
151        """Alias for info property."""
152        return self.info

Alias for info property.

performance: dict[str, typing.Any]
154    @property
155    def performance(self) -> dict[str, Any]:
156        """
157        Get fund performance metrics only.
158
159        Returns:
160            Dictionary with performance data:
161            - daily_return: Daily return
162            - return_1m, return_3m, return_6m: Period returns
163            - return_ytd: Year-to-date return
164            - return_1y, return_3y, return_5y: Annual returns
165        """
166        info = self.info
167        return {
168            "daily_return": info.get("daily_return"),
169            "return_1m": info.get("return_1m"),
170            "return_3m": info.get("return_3m"),
171            "return_6m": info.get("return_6m"),
172            "return_ytd": info.get("return_ytd"),
173            "return_1y": info.get("return_1y"),
174            "return_3y": info.get("return_3y"),
175            "return_5y": info.get("return_5y"),
176        }

Get fund performance metrics only.

Returns: Dictionary with performance data: - daily_return: Daily return - return_1m, return_3m, return_6m: Period returns - return_ytd: Year-to-date return - return_1y, return_3y, return_5y: Annual returns

allocation: pandas.core.frame.DataFrame
178    @property
179    def allocation(self) -> pd.DataFrame:
180        """
181        Get current portfolio allocation (asset breakdown) for last 7 days.
182
183        For longer periods, use allocation_history() method.
184
185        Returns:
186            DataFrame with columns: Date, asset_type, asset_name, weight.
187
188        Examples:
189            >>> fund = Fund("AAK")
190            >>> fund.allocation
191                             Date asset_type         asset_name  weight
192            0 2024-12-20         HS        Hisse Senedi   45.32
193            1 2024-12-20         DB        Devlet Bonusu  30.15
194            ...
195        """
196        return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type)

Get current portfolio allocation (asset breakdown) for last 7 days.

For longer periods, use allocation_history() method.

Returns: DataFrame with columns: Date, asset_type, asset_name, weight.

Examples:

fund = Fund("AAK") fund.allocation Date asset_type asset_name weight 0 2024-12-20 HS Hisse Senedi 45.32 1 2024-12-20 DB Devlet Bonusu 30.15 ...

def allocation_history( self, period: str = '1mo', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
198    def allocation_history(
199        self,
200        period: str = "1mo",
201        start: datetime | str | None = None,
202        end: datetime | str | None = None,
203    ) -> pd.DataFrame:
204        """
205        Get historical portfolio allocation (asset breakdown).
206
207        Note: TEFAS API supports maximum ~100 days (3 months) of data.
208
209        Args:
210            period: How much data to fetch. Valid periods:
211                    1d, 5d, 1mo, 3mo (max ~100 days).
212                    Ignored if start is provided.
213            start: Start date (string or datetime).
214            end: End date (string or datetime). Defaults to today.
215
216        Returns:
217            DataFrame with columns: Date, asset_type, asset_name, weight.
218
219        Examples:
220            >>> fund = Fund("AAK")
221            >>> fund.allocation_history(period="1mo")  # Last month
222            >>> fund.allocation_history(period="3mo")  # Last 3 months (max)
223            >>> fund.allocation_history(start="2024-10-01", end="2024-12-31")
224        """
225        start_dt = self._parse_date(start) if start else None
226        end_dt = self._parse_date(end) if end else None
227
228        # If no start date, calculate from period
229        if start_dt is None:
230            from datetime import timedelta
231            end_dt = end_dt or datetime.now()
232            days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30)
233            # Cap at 100 days (API limit)
234            days = min(days, 100)
235            start_dt = end_dt - timedelta(days=days)
236
237        return self._provider.get_allocation(
238            fund_code=self._fund_code,
239            start=start_dt,
240            end=end_dt,
241            fund_type=self.fund_type,
242        )

Get historical portfolio allocation (asset breakdown).

Note: TEFAS API supports maximum ~100 days (3 months) of data.

Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo (max ~100 days). Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to today.

Returns: DataFrame with columns: Date, asset_type, asset_name, weight.

Examples:

fund = Fund("AAK") fund.allocation_history(period="1mo") # Last month fund.allocation_history(period="3mo") # Last 3 months (max) fund.allocation_history(start="2024-10-01", end="2024-12-31")

def history( self, period: str = '1mo', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
244    def history(
245        self,
246        period: str = "1mo",
247        start: datetime | str | None = None,
248        end: datetime | str | None = None,
249    ) -> pd.DataFrame:
250        """
251        Get historical price data.
252
253        Args:
254            period: How much data to fetch. Valid periods:
255                    1d, 5d, 1mo, 3mo, 6mo, 1y.
256                    Ignored if start is provided.
257            start: Start date (string or datetime).
258            end: End date (string or datetime). Defaults to now.
259
260        Returns:
261            DataFrame with columns: Price, FundSize, Investors.
262            Index is the Date.
263
264        Examples:
265            >>> fund = Fund("AAK")
266            >>> fund.history(period="1mo")  # Last month
267            >>> fund.history(period="1y")  # Last year
268            >>> fund.history(start="2024-01-01", end="2024-06-30")  # Date range
269        """
270        start_dt = self._parse_date(start) if start else None
271        end_dt = self._parse_date(end) if end else None
272
273        return self._provider.get_history(
274            fund_code=self._fund_code,
275            period=period,
276            start=start_dt,
277            end=end_dt,
278            fund_type=self.fund_type,
279        )

Get historical price data.

Args: period: How much data to fetch. Valid periods: 1d, 5d, 1mo, 3mo, 6mo, 1y. Ignored if start is provided. start: Start date (string or datetime). end: End date (string or datetime). Defaults to now.

Returns: DataFrame with columns: Price, FundSize, Investors. Index is the Date.

Examples:

fund = Fund("AAK") fund.history(period="1mo") # Last month fund.history(period="1y") # Last year fund.history(start="2024-01-01", end="2024-06-30") # Date range

def sharpe_ratio(self, period: str = '1y', risk_free_rate: float | None = None) -> float:
292    def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float:
293        """
294        Calculate the Sharpe ratio for the fund.
295
296        Sharpe Ratio = (Rp - Rf) / σp
297        Where:
298        - Rp = Annualized return of the fund
299        - Rf = Risk-free rate (default: 10Y government bond yield)
300        - σp = Annualized standard deviation of returns
301
302        Args:
303            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
304            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
305                           If None, uses current 10Y bond yield from bp.risk_free_rate().
306
307        Returns:
308            Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).
309
310        Examples:
311            >>> fund = bp.Fund("YAY")
312            >>> fund.sharpe_ratio()  # 1-year Sharpe with current risk-free rate
313            0.85
314
315            >>> fund.sharpe_ratio(period="3y")  # 3-year Sharpe
316            1.23
317
318            >>> fund.sharpe_ratio(risk_free_rate=0.25)  # Custom risk-free rate
319            0.92
320        """
321        metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate)
322        return metrics.get("sharpe_ratio", np.nan)

Calculate the Sharpe ratio for the fund.

Sharpe Ratio = (Rp - Rf) / σp Where:

  • Rp = Annualized return of the fund
  • Rf = Risk-free rate (default: 10Y government bond yield)
  • σp = Annualized standard deviation of returns

Args: period: Period for calculation ("1y", "3y", "5y"). Default is "1y". risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield from bp.risk_free_rate().

Returns: Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).

Examples:

fund = bp.Fund("YAY") fund.sharpe_ratio() # 1-year Sharpe with current risk-free rate 0.85

>>> fund.sharpe_ratio(period="3y")  # 3-year Sharpe
1.23

>>> fund.sharpe_ratio(risk_free_rate=0.25)  # Custom risk-free rate
0.92
def risk_metrics( self, period: str = '1y', risk_free_rate: float | None = None) -> dict[str, typing.Any]:
324    def risk_metrics(
325        self,
326        period: str = "1y",
327        risk_free_rate: float | None = None,
328    ) -> dict[str, Any]:
329        """
330        Calculate comprehensive risk metrics for the fund.
331
332        Args:
333            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
334            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
335                           If None, uses current 10Y bond yield.
336
337        Returns:
338            Dictionary with risk metrics:
339            - annualized_return: Annualized return (%)
340            - annualized_volatility: Annualized standard deviation (%)
341            - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp
342            - sortino_ratio: Downside risk-adjusted return
343            - max_drawdown: Maximum peak-to-trough decline (%)
344            - risk_free_rate: Risk-free rate used (%)
345            - trading_days: Number of trading days in the period
346
347        Examples:
348            >>> fund = bp.Fund("YAY")
349            >>> metrics = fund.risk_metrics()
350            >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}")
351            >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")
352        """
353        # Get historical data
354        df = self.history(period=period)
355
356        if df.empty or len(df) < 20:
357            return {
358                "annualized_return": np.nan,
359                "annualized_volatility": np.nan,
360                "sharpe_ratio": np.nan,
361                "sortino_ratio": np.nan,
362                "max_drawdown": np.nan,
363                "risk_free_rate": np.nan,
364                "trading_days": 0,
365            }
366
367        # Calculate daily returns
368        prices = df["Price"]
369        daily_returns = prices.pct_change().dropna()
370        trading_days = len(daily_returns)
371
372        # Annualization factor (trading days per year)
373        annualization_factor = 252
374
375        # Annualized return
376        total_return = (prices.iloc[-1] / prices.iloc[0]) - 1
377        years = trading_days / annualization_factor
378        annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100
379
380        # Annualized volatility
381        daily_volatility = daily_returns.std()
382        annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100
383
384        # Get risk-free rate
385        if risk_free_rate is None:
386            try:
387                from borsapy.bond import risk_free_rate as get_rf_rate
388                rf = get_rf_rate() * 100  # Returns decimal like 0.28, convert to %
389            except Exception:
390                rf = 30.0  # Fallback: approximate Turkish 10Y yield
391        else:
392            rf = risk_free_rate * 100  # Convert decimal to percentage
393
394        # Sharpe Ratio
395        if annualized_volatility > 0:
396            sharpe = (annualized_return - rf) / annualized_volatility
397        else:
398            sharpe = np.nan
399
400        # Sortino Ratio (uses downside deviation)
401        negative_returns = daily_returns[daily_returns < 0]
402        if len(negative_returns) > 0:
403            downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100
404            if downside_deviation > 0:
405                sortino = (annualized_return - rf) / downside_deviation
406            else:
407                sortino = np.nan
408        else:
409            sortino = np.inf  # No negative returns
410
411        # Maximum Drawdown
412        cumulative = (1 + daily_returns).cumprod()
413        running_max = cumulative.cummax()
414        drawdowns = (cumulative - running_max) / running_max
415        max_drawdown = drawdowns.min() * 100  # Negative percentage
416
417        return {
418            "annualized_return": round(annualized_return, 2),
419            "annualized_volatility": round(annualized_volatility, 2),
420            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
421            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
422            "max_drawdown": round(max_drawdown, 2),
423            "risk_free_rate": round(rf, 2),
424            "trading_days": trading_days,
425        }

Calculate comprehensive risk metrics for the fund.

Args: period: Period for calculation ("1y", "3y", "5y"). Default is "1y". risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield.

Returns: Dictionary with risk metrics: - annualized_return: Annualized return (%) - annualized_volatility: Annualized standard deviation (%) - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp - sortino_ratio: Downside risk-adjusted return - max_drawdown: Maximum peak-to-trough decline (%) - risk_free_rate: Risk-free rate used (%) - trading_days: Number of trading days in the period

Examples:

fund = bp.Fund("YAY") metrics = fund.risk_metrics() print(f"Sharpe: {metrics['sharpe_ratio']:.2f}") print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")

def get_holdings( self, api_key: str, period: str | None = None) -> pandas.core.frame.DataFrame:
427    def get_holdings(
428        self,
429        api_key: str,
430        period: str | None = None,
431    ) -> pd.DataFrame:
432        """
433        Get detailed portfolio holdings (individual securities).
434
435        Returns the specific stocks, ETFs, and funds held by this fund,
436        with their weights and ISIN codes. Data is sourced from KAP
437        "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures.
438
439        Uses OpenRouter LLM for PDF parsing.
440
441        Args:
442            api_key: OpenRouter API key for LLM parsing.
443                    Get your free API key at: https://openrouter.ai/
444            period: Optional period in format "YYYY-MM" (e.g., "2025-12").
445                   If None, returns the most recent holdings.
446
447        Returns:
448            DataFrame with columns:
449            - symbol: Security symbol (e.g., "GOOGL", "THYAO")
450            - isin: ISIN code
451            - name: Full security name
452            - weight: Portfolio weight (%)
453            - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.)
454            - country: Country ('TR', 'US', or None)
455            - value: Market value in TRY
456
457        Raises:
458            DataNotAvailableError: If holdings data not available.
459            APIError: If LLM parsing fails.
460            ImportError: If required packages are not installed.
461
462        Examples:
463            >>> fund = bp.Fund("YAY")
464            >>> fund.get_holdings(api_key="sk-or-v1-...")
465               symbol              isin                              name  weight   type country         value
466            0   GOOGL  US02079K3059             ALPHABET INC CL A    6.76  stock      US  82478088.0
467            1    AVGO  US11135F1012             BROADCOM INC          5.11  stock      US  62345678.0
468            ...
469
470            >>> # Get holdings for specific period
471            >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12")
472
473            >>> # Filter by type
474            >>> holdings = fund.get_holdings(api_key="sk-or-v1-...")
475            >>> holdings[holdings['type'] == 'stock']
476        """
477        from borsapy._providers.kap_holdings import get_kap_holdings_provider
478
479        provider = get_kap_holdings_provider()
480        return provider.get_holdings_df(self._fund_code, api_key, period=period)

Get detailed portfolio holdings (individual securities).

Returns the specific stocks, ETFs, and funds held by this fund, with their weights and ISIN codes. Data is sourced from KAP "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures.

Uses OpenRouter LLM for PDF parsing.

Args: api_key: OpenRouter API key for LLM parsing. Get your free API key at: https://openrouter.ai/ period: Optional period in format "YYYY-MM" (e.g., "2025-12"). If None, returns the most recent holdings.

Returns: DataFrame with columns: - symbol: Security symbol (e.g., "GOOGL", "THYAO") - isin: ISIN code - name: Full security name - weight: Portfolio weight (%) - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.) - country: Country ('TR', 'US', or None) - value: Market value in TRY

Raises: DataNotAvailableError: If holdings data not available. APIError: If LLM parsing fails. ImportError: If required packages are not installed.

Examples:

fund = bp.Fund("YAY") fund.get_holdings(api_key="sk-or-v1-...") symbol isin name weight type country value 0 GOOGL US02079K3059 ALPHABET INC CL A 6.76 stock US 82478088.0 1 AVGO US11135F1012 BROADCOM INC 5.11 stock US 62345678.0 ...

>>> # Get holdings for specific period
>>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12")

>>> # Filter by type
>>> holdings = fund.get_holdings(api_key="sk-or-v1-...")
>>> holdings[holdings['type'] == 'stock']
class Portfolio(borsapy.technical.TechnicalMixin):
120class Portfolio(TechnicalMixin):
121    """
122    Multi-asset portfolio management with performance tracking and risk metrics.
123
124    Supports 4 asset types:
125    - stock: BIST stocks via Ticker class
126    - fx: Currencies, metals, commodities via FX class
127    - crypto: Cryptocurrencies via Crypto class
128    - fund: TEFAS mutual funds via Fund class
129
130    Examples:
131        >>> import borsapy as bp
132        >>> p = bp.Portfolio()
133        >>> p.add("THYAO", shares=100, cost=280)
134        >>> p.add("gram-altin", shares=5, asset_type="fx")
135        >>> p.add("YAY", shares=500, asset_type="fund")
136        >>> p.set_benchmark("XU100")
137        >>> print(p.holdings)
138        >>> print(f"Value: {p.value:,.2f} TL")
139        >>> print(f"Sharpe: {p.risk_metrics()['sharpe_ratio']:.2f}")
140    """
141
142    def __init__(self, benchmark: str = "XU100"):
143        """
144        Initialize an empty portfolio.
145
146        Args:
147            benchmark: Index symbol for beta/alpha calculations.
148                       Default is XU100 (BIST 100).
149        """
150        self._holdings: dict[str, Holding] = {}
151        self._asset_cache: dict[str, Ticker | FX | Crypto | Fund] = {}
152        self._benchmark = benchmark
153
154    # === Asset Management ===
155
156    def add(
157        self,
158        symbol: str,
159        shares: float,
160        cost: float | None = None,
161        asset_type: str | None = None,
162        purchase_date: str | date | datetime | None = None,
163    ) -> "Portfolio":
164        """
165        Add an asset to the portfolio.
166
167        Args:
168            symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.)
169            shares: Number of shares/units.
170            cost: Cost per share/unit. If None, uses current price.
171            asset_type: Asset type override. Auto-detected if None.
172                        Valid values: "stock", "fx", "crypto", "fund"
173            purchase_date: Date when the asset was purchased.
174                          Accepts string (YYYY-MM-DD), date, or datetime.
175                          If None, defaults to today.
176
177        Returns:
178            Self for method chaining.
179
180        Examples:
181            >>> p = Portfolio()
182            >>> p.add("THYAO", shares=100, cost=280)  # Stock with cost
183            >>> p.add("GARAN", shares=200)  # Stock at current price
184            >>> p.add("gram-altin", shares=5, asset_type="fx")  # Metal
185            >>> p.add("YAY", shares=500, asset_type="fund")  # Mutual fund
186            >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15")
187        """
188        symbol = symbol.upper() if asset_type != "fx" else symbol
189
190        # Detect or validate asset type
191        if asset_type is None:
192            detected_type = _detect_asset_type(symbol)
193        else:
194            detected_type = asset_type  # type: ignore
195
196        # Get current price if cost not provided
197        if cost is None:
198            asset = self._get_or_create_asset(symbol, detected_type)
199            cost = self._get_current_price(asset)
200
201        # Parse purchase_date
202        parsed_date: date | None = None
203        if purchase_date is not None:
204            if isinstance(purchase_date, str):
205                parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date()
206            elif isinstance(purchase_date, datetime):
207                parsed_date = purchase_date.date()
208            elif isinstance(purchase_date, date):
209                parsed_date = purchase_date
210        else:
211            parsed_date = date.today()
212
213        self._holdings[symbol] = Holding(
214            symbol=symbol,
215            shares=shares,
216            cost_per_share=cost,
217            asset_type=detected_type,
218            purchase_date=parsed_date,
219        )
220
221        return self
222
223    def remove(self, symbol: str) -> "Portfolio":
224        """
225        Remove an asset from the portfolio.
226
227        Args:
228            symbol: Asset symbol to remove.
229
230        Returns:
231            Self for method chaining.
232        """
233        symbol_upper = symbol.upper()
234
235        # Try both original and uppercase
236        if symbol in self._holdings:
237            del self._holdings[symbol]
238            self._asset_cache.pop(symbol, None)
239        elif symbol_upper in self._holdings:
240            del self._holdings[symbol_upper]
241            self._asset_cache.pop(symbol_upper, None)
242
243        return self
244
245    def update(
246        self,
247        symbol: str,
248        shares: float | None = None,
249        cost: float | None = None,
250    ) -> "Portfolio":
251        """
252        Update an existing holding.
253
254        Args:
255            symbol: Asset symbol.
256            shares: New share count. If None, keeps existing.
257            cost: New cost per share. If None, keeps existing.
258
259        Returns:
260            Self for method chaining.
261        """
262        if symbol not in self._holdings:
263            symbol = symbol.upper()
264        if symbol not in self._holdings:
265            raise KeyError(f"Symbol {symbol} not in portfolio")
266
267        holding = self._holdings[symbol]
268        if shares is not None:
269            holding.shares = shares
270        if cost is not None:
271            holding.cost_per_share = cost
272
273        return self
274
275    def clear(self) -> "Portfolio":
276        """
277        Remove all holdings from the portfolio.
278
279        Returns:
280            Self for method chaining.
281        """
282        self._holdings.clear()
283        self._asset_cache.clear()
284        return self
285
286    def set_benchmark(self, index: str) -> "Portfolio":
287        """
288        Set the benchmark index for beta/alpha calculations.
289
290        Args:
291            index: Index symbol (XU100, XU030, XK030, etc.)
292
293        Returns:
294            Self for method chaining.
295        """
296        self._benchmark = index
297        return self
298
299    # === Properties ===
300
301    @property
302    def holdings(self) -> pd.DataFrame:
303        """
304        Get all holdings as a DataFrame.
305
306        Returns:
307            DataFrame with columns:
308            - symbol: Asset symbol
309            - shares: Number of shares
310            - cost: Cost per share
311            - current_price: Current price
312            - value: Current value (shares * price)
313            - weight: Portfolio weight (%)
314            - pnl: Profit/loss (TL)
315            - pnl_pct: Profit/loss (%)
316            - asset_type: Asset type
317            - purchase_date: Date when asset was purchased
318            - holding_days: Number of days since purchase
319        """
320        if not self._holdings:
321            return pd.DataFrame(
322                columns=[
323                    "symbol", "shares", "cost", "current_price",
324                    "value", "weight", "pnl", "pnl_pct", "asset_type",
325                    "purchase_date", "holding_days"
326                ]
327            )
328
329        rows = []
330        total_value = self.value
331        today = date.today()
332
333        for symbol, holding in self._holdings.items():
334            asset = self._get_or_create_asset(symbol, holding.asset_type)
335            current_price = self._get_current_price(asset)
336            value = holding.shares * current_price
337            cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0
338            pnl = value - cost_basis if cost_basis else 0
339            pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0
340            weight = (value / total_value * 100) if total_value else 0
341
342            # Calculate holding days
343            holding_days = None
344            if holding.purchase_date:
345                holding_days = (today - holding.purchase_date).days
346
347            rows.append({
348                "symbol": symbol,
349                "shares": holding.shares,
350                "cost": holding.cost_per_share,
351                "current_price": current_price,
352                "value": value,
353                "weight": round(weight, 2),
354                "pnl": round(pnl, 2),
355                "pnl_pct": round(pnl_pct, 2),
356                "asset_type": holding.asset_type,
357                "purchase_date": holding.purchase_date,
358                "holding_days": holding_days,
359            })
360
361        return pd.DataFrame(rows)
362
363    @property
364    def symbols(self) -> list[str]:
365        """Get list of symbols in portfolio."""
366        return list(self._holdings.keys())
367
368    @property
369    def value(self) -> float:
370        """Get total portfolio value in TL."""
371        total = 0.0
372        for symbol, holding in self._holdings.items():
373            asset = self._get_or_create_asset(symbol, holding.asset_type)
374            price = self._get_current_price(asset)
375            total += holding.shares * price
376        return total
377
378    @property
379    def cost(self) -> float:
380        """Get total portfolio cost basis in TL."""
381        total = 0.0
382        for holding in self._holdings.values():
383            if holding.cost_per_share:
384                total += holding.shares * holding.cost_per_share
385        return total
386
387    @property
388    def pnl(self) -> float:
389        """Get total profit/loss in TL."""
390        return self.value - self.cost
391
392    @property
393    def pnl_pct(self) -> float:
394        """Get total profit/loss as percentage."""
395        cost = self.cost
396        if cost == 0:
397            return 0.0
398        return (self.pnl / cost) * 100
399
400    @property
401    def weights(self) -> dict[str, float]:
402        """Get portfolio weights as dictionary."""
403        total_value = self.value
404        if total_value == 0:
405            return {}
406
407        result = {}
408        for symbol, holding in self._holdings.items():
409            asset = self._get_or_create_asset(symbol, holding.asset_type)
410            price = self._get_current_price(asset)
411            value = holding.shares * price
412            result[symbol] = round(value / total_value, 4)
413        return result
414
415    # === Performance ===
416
417    def history(self, period: str = "1y") -> pd.DataFrame:
418        """
419        Get historical portfolio value based on current holdings.
420
421        Note: Uses current share counts - does not track historical trades.
422        When purchase_date is set for a holding, only data from that date
423        onwards is included in the portfolio value calculation.
424
425        Args:
426            period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).
427
428        Returns:
429            DataFrame with columns: Value, Daily_Return.
430            Index is Date.
431        """
432        if not self._holdings:
433            return pd.DataFrame(columns=["Value", "Daily_Return"])
434
435        all_prices = {}
436        for symbol, holding in self._holdings.items():
437            asset = self._get_or_create_asset(symbol, holding.asset_type)
438            try:
439                hist = asset.history(period=period)
440                if hist.empty:
441                    continue
442
443                # Filter by purchase_date if set
444                if holding.purchase_date:
445                    # Handle both timezone-aware and timezone-naive indices
446                    if hasattr(hist.index, 'tz') and hist.index.tz is not None:
447                        hist = hist[hist.index.date >= holding.purchase_date]
448                    else:
449                        hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)]
450
451                if hist.empty:
452                    continue
453
454                # Use Close for stocks/index, Price for funds
455                price_col = "Close" if "Close" in hist.columns else "Price"
456                all_prices[symbol] = hist[price_col] * holding.shares
457            except Exception:
458                continue
459
460        if not all_prices:
461            return pd.DataFrame(columns=["Value", "Daily_Return"])
462
463        df = pd.DataFrame(all_prices)
464        df = df.dropna(how="all")
465        df["Value"] = df.sum(axis=1)
466        df["Daily_Return"] = df["Value"].pct_change()
467        return df[["Value", "Daily_Return"]]
468
469    @property
470    def performance(self) -> dict[str, float]:
471        """
472        Get portfolio performance summary.
473
474        Returns:
475            Dictionary with:
476            - total_return: Total return (%)
477            - annualized_return: Annualized return (%)
478            - total_value: Current value (TL)
479            - total_cost: Total cost (TL)
480            - total_pnl: Profit/loss (TL)
481        """
482        return {
483            "total_return": self.pnl_pct,
484            "annualized_return": np.nan,  # Calculated in risk_metrics
485            "total_value": self.value,
486            "total_cost": self.cost,
487            "total_pnl": self.pnl,
488        }
489
490    # === Risk Metrics ===
491
492    def risk_metrics(
493        self,
494        period: str = "1y",
495        risk_free_rate: float | None = None,
496    ) -> dict[str, Any]:
497        """
498        Calculate comprehensive risk metrics.
499
500        Args:
501            period: Period for calculation (1y, 3mo, 6mo).
502            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
503                           If None, uses current 10Y bond yield.
504
505        Returns:
506            Dictionary with:
507            - annualized_return: Annualized return (%)
508            - annualized_volatility: Annualized volatility (%)
509            - sharpe_ratio: Risk-adjusted return
510            - sortino_ratio: Downside risk-adjusted return
511            - max_drawdown: Maximum drawdown (%)
512            - beta: Beta vs benchmark
513            - alpha: Alpha vs benchmark (%)
514            - risk_free_rate: Risk-free rate used (%)
515            - trading_days: Number of trading days
516        """
517        df = self.history(period=period)
518
519        if df.empty or len(df) < 20:
520            return {
521                "annualized_return": np.nan,
522                "annualized_volatility": np.nan,
523                "sharpe_ratio": np.nan,
524                "sortino_ratio": np.nan,
525                "max_drawdown": np.nan,
526                "beta": np.nan,
527                "alpha": np.nan,
528                "risk_free_rate": np.nan,
529                "trading_days": 0,
530            }
531
532        daily_returns = df["Daily_Return"].dropna()
533        trading_days = len(daily_returns)
534        annualization = 252
535
536        # Annualized return
537        total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1
538        years = trading_days / annualization
539        ann_return = ((1 + total_return) ** (1 / years) - 1) * 100
540
541        # Annualized volatility
542        daily_volatility = daily_returns.std()
543        ann_volatility = daily_volatility * np.sqrt(annualization) * 100
544
545        # Get risk-free rate
546        if risk_free_rate is None:
547            try:
548                from borsapy.bond import risk_free_rate as get_rf_rate
549                rf = get_rf_rate() * 100  # Convert to percentage
550            except Exception:
551                rf = 30.0  # Fallback
552        else:
553            rf = risk_free_rate * 100
554
555        # Sharpe Ratio
556        if ann_volatility > 0:
557            sharpe = (ann_return - rf) / ann_volatility
558        else:
559            sharpe = np.nan
560
561        # Sortino Ratio (downside deviation)
562        negative_returns = daily_returns[daily_returns < 0]
563        if len(negative_returns) > 0:
564            downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100
565            if downside_deviation > 0:
566                sortino = (ann_return - rf) / downside_deviation
567            else:
568                sortino = np.nan
569        else:
570            sortino = np.inf  # No negative returns
571
572        # Maximum Drawdown
573        cumulative = (1 + daily_returns).cumprod()
574        running_max = cumulative.cummax()
575        drawdowns = (cumulative - running_max) / running_max
576        max_drawdown = drawdowns.min() * 100
577
578        # Beta and Alpha (vs benchmark)
579        beta = np.nan
580        alpha = np.nan
581
582        try:
583            bench = Index(self._benchmark)
584            bench_hist = bench.history(period=period)
585            if not bench_hist.empty:
586                bench_returns = bench_hist["Close"].pct_change().dropna()
587
588                # Align dates
589                common_dates = daily_returns.index.intersection(bench_returns.index)
590                if len(common_dates) >= 20:
591                    port_ret = daily_returns.loc[common_dates]
592                    bench_ret = bench_returns.loc[common_dates]
593
594                    # Beta = Cov(Rp, Rm) / Var(Rm)
595                    covariance = port_ret.cov(bench_ret)
596                    variance = bench_ret.var()
597                    if variance > 0:
598                        beta = covariance / variance
599
600                        # Alpha = Rp - Rf - Beta * (Rm - Rf)
601                        bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1
602                        bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100
603                        alpha = ann_return - rf - beta * (bench_ann - rf)
604        except Exception:
605            pass
606
607        return {
608            "annualized_return": round(ann_return, 2),
609            "annualized_volatility": round(ann_volatility, 2),
610            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
611            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
612            "max_drawdown": round(max_drawdown, 2),
613            "beta": round(beta, 2) if not np.isnan(beta) else np.nan,
614            "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan,
615            "risk_free_rate": round(rf, 2),
616            "trading_days": trading_days,
617        }
618
619    def sharpe_ratio(self, period: str = "1y") -> float:
620        """
621        Calculate Sharpe ratio.
622
623        Args:
624            period: Period for calculation.
625
626        Returns:
627            Sharpe ratio.
628        """
629        return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)
630
631    def sortino_ratio(self, period: str = "1y") -> float:
632        """
633        Calculate Sortino ratio.
634
635        Args:
636            period: Period for calculation.
637
638        Returns:
639            Sortino ratio.
640        """
641        return self.risk_metrics(period=period).get("sortino_ratio", np.nan)
642
643    def beta(self, benchmark: str | None = None, period: str = "1y") -> float:
644        """
645        Calculate beta vs benchmark.
646
647        Args:
648            benchmark: Benchmark index. Uses portfolio default if None.
649            period: Period for calculation.
650
651        Returns:
652            Beta coefficient.
653        """
654        if benchmark:
655            old_bench = self._benchmark
656            self._benchmark = benchmark
657            result = self.risk_metrics(period=period).get("beta", np.nan)
658            self._benchmark = old_bench
659            return result
660        return self.risk_metrics(period=period).get("beta", np.nan)
661
662    def correlation_matrix(self, period: str = "1y") -> pd.DataFrame:
663        """
664        Calculate correlation matrix between holdings.
665
666        Args:
667            period: Period for calculation.
668
669        Returns:
670            DataFrame with correlation coefficients.
671        """
672        if len(self._holdings) < 2:
673            return pd.DataFrame()
674
675        returns_dict = {}
676        for symbol, holding in self._holdings.items():
677            try:
678                asset = self._get_or_create_asset(symbol, holding.asset_type)
679                hist = asset.history(period=period)
680                if hist.empty:
681                    continue
682                price_col = "Close" if "Close" in hist.columns else "Price"
683                returns_dict[symbol] = hist[price_col].pct_change()
684            except Exception:
685                continue
686
687        if len(returns_dict) < 2:
688            return pd.DataFrame()
689
690        df = pd.DataFrame(returns_dict).dropna()
691        return df.corr()
692
693    # === Import/Export ===
694
695    def to_dict(self) -> dict[str, Any]:
696        """
697        Export portfolio to dictionary.
698
699        Returns:
700            Dictionary with portfolio data.
701        """
702        return {
703            "benchmark": self._benchmark,
704            "holdings": [
705                {
706                    "symbol": h.symbol,
707                    "shares": h.shares,
708                    "cost_per_share": h.cost_per_share,
709                    "asset_type": h.asset_type,
710                    "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None,
711                }
712                for h in self._holdings.values()
713            ],
714        }
715
716    @classmethod
717    def from_dict(cls, data: dict[str, Any]) -> "Portfolio":
718        """
719        Create portfolio from dictionary.
720
721        Args:
722            data: Dictionary with portfolio data.
723
724        Returns:
725            Portfolio instance.
726        """
727        portfolio = cls(benchmark=data.get("benchmark", "XU100"))
728        for h in data.get("holdings", []):
729            # Parse purchase_date from ISO string
730            purchase_date = None
731            if h.get("purchase_date"):
732                purchase_date = date.fromisoformat(h["purchase_date"])
733
734            portfolio.add(
735                symbol=h["symbol"],
736                shares=h["shares"],
737                cost=h.get("cost_per_share"),
738                asset_type=h.get("asset_type"),
739                purchase_date=purchase_date,
740            )
741        return portfolio
742
743    # === Private Methods ===
744
745    def _get_or_create_asset(
746        self, symbol: str, asset_type: AssetType
747    ) -> Ticker | FX | Crypto | Fund:
748        """Get or create asset instance from cache."""
749        cache_key = f"{symbol}_{asset_type}"
750        if cache_key not in self._asset_cache:
751            self._asset_cache[cache_key] = _get_asset(symbol, asset_type)
752        return self._asset_cache[cache_key]
753
754    def _get_current_price(self, asset: Ticker | FX | Crypto | Fund) -> float:
755        """Get current price from asset."""
756        try:
757            if isinstance(asset, Ticker):
758                return asset.fast_info.last_price or 0
759            elif isinstance(asset, Crypto):
760                return asset.fast_info.last_price or 0
761            elif isinstance(asset, FX):
762                current = asset.current
763                return current.get("last", 0) if current else 0
764            elif isinstance(asset, Fund):
765                info = asset.info
766                return info.get("price", 0) if info else 0
767        except Exception:
768            pass
769        return 0
770
771    def __repr__(self) -> str:
772        n = len(self._holdings)
773        value = self.value
774        return f"Portfolio({n} holdings, {value:,.2f} TL)"
775
776    def __len__(self) -> int:
777        return len(self._holdings)

Multi-asset portfolio management with performance tracking and risk metrics.

Supports 4 asset types:

  • stock: BIST stocks via Ticker class
  • fx: Currencies, metals, commodities via FX class
  • crypto: Cryptocurrencies via Crypto class
  • fund: TEFAS mutual funds via Fund class

Examples:

import borsapy as bp p = bp.Portfolio() p.add("THYAO", shares=100, cost=280) p.add("gram-altin", shares=5, asset_type="fx") p.add("YAY", shares=500, asset_type="fund") p.set_benchmark("XU100") print(p.holdings) print(f"Value: {p.value:,.2f} TL") print(f"Sharpe: {p.risk_metrics()['sharpe_ratio']:.2f}")

Portfolio(benchmark: str = 'XU100')
142    def __init__(self, benchmark: str = "XU100"):
143        """
144        Initialize an empty portfolio.
145
146        Args:
147            benchmark: Index symbol for beta/alpha calculations.
148                       Default is XU100 (BIST 100).
149        """
150        self._holdings: dict[str, Holding] = {}
151        self._asset_cache: dict[str, Ticker | FX | Crypto | Fund] = {}
152        self._benchmark = benchmark

Initialize an empty portfolio.

Args: benchmark: Index symbol for beta/alpha calculations. Default is XU100 (BIST 100).

def add( self, symbol: str, shares: float, cost: float | None = None, asset_type: str | None = None, purchase_date: str | datetime.date | datetime.datetime | None = None) -> Portfolio:
156    def add(
157        self,
158        symbol: str,
159        shares: float,
160        cost: float | None = None,
161        asset_type: str | None = None,
162        purchase_date: str | date | datetime | None = None,
163    ) -> "Portfolio":
164        """
165        Add an asset to the portfolio.
166
167        Args:
168            symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.)
169            shares: Number of shares/units.
170            cost: Cost per share/unit. If None, uses current price.
171            asset_type: Asset type override. Auto-detected if None.
172                        Valid values: "stock", "fx", "crypto", "fund"
173            purchase_date: Date when the asset was purchased.
174                          Accepts string (YYYY-MM-DD), date, or datetime.
175                          If None, defaults to today.
176
177        Returns:
178            Self for method chaining.
179
180        Examples:
181            >>> p = Portfolio()
182            >>> p.add("THYAO", shares=100, cost=280)  # Stock with cost
183            >>> p.add("GARAN", shares=200)  # Stock at current price
184            >>> p.add("gram-altin", shares=5, asset_type="fx")  # Metal
185            >>> p.add("YAY", shares=500, asset_type="fund")  # Mutual fund
186            >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15")
187        """
188        symbol = symbol.upper() if asset_type != "fx" else symbol
189
190        # Detect or validate asset type
191        if asset_type is None:
192            detected_type = _detect_asset_type(symbol)
193        else:
194            detected_type = asset_type  # type: ignore
195
196        # Get current price if cost not provided
197        if cost is None:
198            asset = self._get_or_create_asset(symbol, detected_type)
199            cost = self._get_current_price(asset)
200
201        # Parse purchase_date
202        parsed_date: date | None = None
203        if purchase_date is not None:
204            if isinstance(purchase_date, str):
205                parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date()
206            elif isinstance(purchase_date, datetime):
207                parsed_date = purchase_date.date()
208            elif isinstance(purchase_date, date):
209                parsed_date = purchase_date
210        else:
211            parsed_date = date.today()
212
213        self._holdings[symbol] = Holding(
214            symbol=symbol,
215            shares=shares,
216            cost_per_share=cost,
217            asset_type=detected_type,
218            purchase_date=parsed_date,
219        )
220
221        return self

Add an asset to the portfolio.

Args: symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.) shares: Number of shares/units. cost: Cost per share/unit. If None, uses current price. asset_type: Asset type override. Auto-detected if None. Valid values: "stock", "fx", "crypto", "fund" purchase_date: Date when the asset was purchased. Accepts string (YYYY-MM-DD), date, or datetime. If None, defaults to today.

Returns: Self for method chaining.

Examples:

p = Portfolio() p.add("THYAO", shares=100, cost=280) # Stock with cost p.add("GARAN", shares=200) # Stock at current price p.add("gram-altin", shares=5, asset_type="fx") # Metal p.add("YAY", shares=500, asset_type="fund") # Mutual fund p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15")

def remove(self, symbol: str) -> Portfolio:
223    def remove(self, symbol: str) -> "Portfolio":
224        """
225        Remove an asset from the portfolio.
226
227        Args:
228            symbol: Asset symbol to remove.
229
230        Returns:
231            Self for method chaining.
232        """
233        symbol_upper = symbol.upper()
234
235        # Try both original and uppercase
236        if symbol in self._holdings:
237            del self._holdings[symbol]
238            self._asset_cache.pop(symbol, None)
239        elif symbol_upper in self._holdings:
240            del self._holdings[symbol_upper]
241            self._asset_cache.pop(symbol_upper, None)
242
243        return self

Remove an asset from the portfolio.

Args: symbol: Asset symbol to remove.

Returns: Self for method chaining.

def update( self, symbol: str, shares: float | None = None, cost: float | None = None) -> Portfolio:
245    def update(
246        self,
247        symbol: str,
248        shares: float | None = None,
249        cost: float | None = None,
250    ) -> "Portfolio":
251        """
252        Update an existing holding.
253
254        Args:
255            symbol: Asset symbol.
256            shares: New share count. If None, keeps existing.
257            cost: New cost per share. If None, keeps existing.
258
259        Returns:
260            Self for method chaining.
261        """
262        if symbol not in self._holdings:
263            symbol = symbol.upper()
264        if symbol not in self._holdings:
265            raise KeyError(f"Symbol {symbol} not in portfolio")
266
267        holding = self._holdings[symbol]
268        if shares is not None:
269            holding.shares = shares
270        if cost is not None:
271            holding.cost_per_share = cost
272
273        return self

Update an existing holding.

Args: symbol: Asset symbol. shares: New share count. If None, keeps existing. cost: New cost per share. If None, keeps existing.

Returns: Self for method chaining.

def clear(self) -> Portfolio:
275    def clear(self) -> "Portfolio":
276        """
277        Remove all holdings from the portfolio.
278
279        Returns:
280            Self for method chaining.
281        """
282        self._holdings.clear()
283        self._asset_cache.clear()
284        return self

Remove all holdings from the portfolio.

Returns: Self for method chaining.

def set_benchmark(self, index: str) -> Portfolio:
286    def set_benchmark(self, index: str) -> "Portfolio":
287        """
288        Set the benchmark index for beta/alpha calculations.
289
290        Args:
291            index: Index symbol (XU100, XU030, XK030, etc.)
292
293        Returns:
294            Self for method chaining.
295        """
296        self._benchmark = index
297        return self

Set the benchmark index for beta/alpha calculations.

Args: index: Index symbol (XU100, XU030, XK030, etc.)

Returns: Self for method chaining.

holdings: pandas.core.frame.DataFrame
301    @property
302    def holdings(self) -> pd.DataFrame:
303        """
304        Get all holdings as a DataFrame.
305
306        Returns:
307            DataFrame with columns:
308            - symbol: Asset symbol
309            - shares: Number of shares
310            - cost: Cost per share
311            - current_price: Current price
312            - value: Current value (shares * price)
313            - weight: Portfolio weight (%)
314            - pnl: Profit/loss (TL)
315            - pnl_pct: Profit/loss (%)
316            - asset_type: Asset type
317            - purchase_date: Date when asset was purchased
318            - holding_days: Number of days since purchase
319        """
320        if not self._holdings:
321            return pd.DataFrame(
322                columns=[
323                    "symbol", "shares", "cost", "current_price",
324                    "value", "weight", "pnl", "pnl_pct", "asset_type",
325                    "purchase_date", "holding_days"
326                ]
327            )
328
329        rows = []
330        total_value = self.value
331        today = date.today()
332
333        for symbol, holding in self._holdings.items():
334            asset = self._get_or_create_asset(symbol, holding.asset_type)
335            current_price = self._get_current_price(asset)
336            value = holding.shares * current_price
337            cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0
338            pnl = value - cost_basis if cost_basis else 0
339            pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0
340            weight = (value / total_value * 100) if total_value else 0
341
342            # Calculate holding days
343            holding_days = None
344            if holding.purchase_date:
345                holding_days = (today - holding.purchase_date).days
346
347            rows.append({
348                "symbol": symbol,
349                "shares": holding.shares,
350                "cost": holding.cost_per_share,
351                "current_price": current_price,
352                "value": value,
353                "weight": round(weight, 2),
354                "pnl": round(pnl, 2),
355                "pnl_pct": round(pnl_pct, 2),
356                "asset_type": holding.asset_type,
357                "purchase_date": holding.purchase_date,
358                "holding_days": holding_days,
359            })
360
361        return pd.DataFrame(rows)

Get all holdings as a DataFrame.

Returns: DataFrame with columns: - symbol: Asset symbol - shares: Number of shares - cost: Cost per share - current_price: Current price - value: Current value (shares * price) - weight: Portfolio weight (%) - pnl: Profit/loss (TL) - pnl_pct: Profit/loss (%) - asset_type: Asset type - purchase_date: Date when asset was purchased - holding_days: Number of days since purchase

symbols: list[str]
363    @property
364    def symbols(self) -> list[str]:
365        """Get list of symbols in portfolio."""
366        return list(self._holdings.keys())

Get list of symbols in portfolio.

value: float
368    @property
369    def value(self) -> float:
370        """Get total portfolio value in TL."""
371        total = 0.0
372        for symbol, holding in self._holdings.items():
373            asset = self._get_or_create_asset(symbol, holding.asset_type)
374            price = self._get_current_price(asset)
375            total += holding.shares * price
376        return total

Get total portfolio value in TL.

cost: float
378    @property
379    def cost(self) -> float:
380        """Get total portfolio cost basis in TL."""
381        total = 0.0
382        for holding in self._holdings.values():
383            if holding.cost_per_share:
384                total += holding.shares * holding.cost_per_share
385        return total

Get total portfolio cost basis in TL.

pnl: float
387    @property
388    def pnl(self) -> float:
389        """Get total profit/loss in TL."""
390        return self.value - self.cost

Get total profit/loss in TL.

pnl_pct: float
392    @property
393    def pnl_pct(self) -> float:
394        """Get total profit/loss as percentage."""
395        cost = self.cost
396        if cost == 0:
397            return 0.0
398        return (self.pnl / cost) * 100

Get total profit/loss as percentage.

weights: dict[str, float]
400    @property
401    def weights(self) -> dict[str, float]:
402        """Get portfolio weights as dictionary."""
403        total_value = self.value
404        if total_value == 0:
405            return {}
406
407        result = {}
408        for symbol, holding in self._holdings.items():
409            asset = self._get_or_create_asset(symbol, holding.asset_type)
410            price = self._get_current_price(asset)
411            value = holding.shares * price
412            result[symbol] = round(value / total_value, 4)
413        return result

Get portfolio weights as dictionary.

def history(self, period: str = '1y') -> pandas.core.frame.DataFrame:
417    def history(self, period: str = "1y") -> pd.DataFrame:
418        """
419        Get historical portfolio value based on current holdings.
420
421        Note: Uses current share counts - does not track historical trades.
422        When purchase_date is set for a holding, only data from that date
423        onwards is included in the portfolio value calculation.
424
425        Args:
426            period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).
427
428        Returns:
429            DataFrame with columns: Value, Daily_Return.
430            Index is Date.
431        """
432        if not self._holdings:
433            return pd.DataFrame(columns=["Value", "Daily_Return"])
434
435        all_prices = {}
436        for symbol, holding in self._holdings.items():
437            asset = self._get_or_create_asset(symbol, holding.asset_type)
438            try:
439                hist = asset.history(period=period)
440                if hist.empty:
441                    continue
442
443                # Filter by purchase_date if set
444                if holding.purchase_date:
445                    # Handle both timezone-aware and timezone-naive indices
446                    if hasattr(hist.index, 'tz') and hist.index.tz is not None:
447                        hist = hist[hist.index.date >= holding.purchase_date]
448                    else:
449                        hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)]
450
451                if hist.empty:
452                    continue
453
454                # Use Close for stocks/index, Price for funds
455                price_col = "Close" if "Close" in hist.columns else "Price"
456                all_prices[symbol] = hist[price_col] * holding.shares
457            except Exception:
458                continue
459
460        if not all_prices:
461            return pd.DataFrame(columns=["Value", "Daily_Return"])
462
463        df = pd.DataFrame(all_prices)
464        df = df.dropna(how="all")
465        df["Value"] = df.sum(axis=1)
466        df["Daily_Return"] = df["Value"].pct_change()
467        return df[["Value", "Daily_Return"]]

Get historical portfolio value based on current holdings.

Note: Uses current share counts - does not track historical trades. When purchase_date is set for a holding, only data from that date onwards is included in the portfolio value calculation.

Args: period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).

Returns: DataFrame with columns: Value, Daily_Return. Index is Date.

performance: dict[str, float]
469    @property
470    def performance(self) -> dict[str, float]:
471        """
472        Get portfolio performance summary.
473
474        Returns:
475            Dictionary with:
476            - total_return: Total return (%)
477            - annualized_return: Annualized return (%)
478            - total_value: Current value (TL)
479            - total_cost: Total cost (TL)
480            - total_pnl: Profit/loss (TL)
481        """
482        return {
483            "total_return": self.pnl_pct,
484            "annualized_return": np.nan,  # Calculated in risk_metrics
485            "total_value": self.value,
486            "total_cost": self.cost,
487            "total_pnl": self.pnl,
488        }

Get portfolio performance summary.

Returns: Dictionary with: - total_return: Total return (%) - annualized_return: Annualized return (%) - total_value: Current value (TL) - total_cost: Total cost (TL) - total_pnl: Profit/loss (TL)

def risk_metrics( self, period: str = '1y', risk_free_rate: float | None = None) -> dict[str, typing.Any]:
492    def risk_metrics(
493        self,
494        period: str = "1y",
495        risk_free_rate: float | None = None,
496    ) -> dict[str, Any]:
497        """
498        Calculate comprehensive risk metrics.
499
500        Args:
501            period: Period for calculation (1y, 3mo, 6mo).
502            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
503                           If None, uses current 10Y bond yield.
504
505        Returns:
506            Dictionary with:
507            - annualized_return: Annualized return (%)
508            - annualized_volatility: Annualized volatility (%)
509            - sharpe_ratio: Risk-adjusted return
510            - sortino_ratio: Downside risk-adjusted return
511            - max_drawdown: Maximum drawdown (%)
512            - beta: Beta vs benchmark
513            - alpha: Alpha vs benchmark (%)
514            - risk_free_rate: Risk-free rate used (%)
515            - trading_days: Number of trading days
516        """
517        df = self.history(period=period)
518
519        if df.empty or len(df) < 20:
520            return {
521                "annualized_return": np.nan,
522                "annualized_volatility": np.nan,
523                "sharpe_ratio": np.nan,
524                "sortino_ratio": np.nan,
525                "max_drawdown": np.nan,
526                "beta": np.nan,
527                "alpha": np.nan,
528                "risk_free_rate": np.nan,
529                "trading_days": 0,
530            }
531
532        daily_returns = df["Daily_Return"].dropna()
533        trading_days = len(daily_returns)
534        annualization = 252
535
536        # Annualized return
537        total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1
538        years = trading_days / annualization
539        ann_return = ((1 + total_return) ** (1 / years) - 1) * 100
540
541        # Annualized volatility
542        daily_volatility = daily_returns.std()
543        ann_volatility = daily_volatility * np.sqrt(annualization) * 100
544
545        # Get risk-free rate
546        if risk_free_rate is None:
547            try:
548                from borsapy.bond import risk_free_rate as get_rf_rate
549                rf = get_rf_rate() * 100  # Convert to percentage
550            except Exception:
551                rf = 30.0  # Fallback
552        else:
553            rf = risk_free_rate * 100
554
555        # Sharpe Ratio
556        if ann_volatility > 0:
557            sharpe = (ann_return - rf) / ann_volatility
558        else:
559            sharpe = np.nan
560
561        # Sortino Ratio (downside deviation)
562        negative_returns = daily_returns[daily_returns < 0]
563        if len(negative_returns) > 0:
564            downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100
565            if downside_deviation > 0:
566                sortino = (ann_return - rf) / downside_deviation
567            else:
568                sortino = np.nan
569        else:
570            sortino = np.inf  # No negative returns
571
572        # Maximum Drawdown
573        cumulative = (1 + daily_returns).cumprod()
574        running_max = cumulative.cummax()
575        drawdowns = (cumulative - running_max) / running_max
576        max_drawdown = drawdowns.min() * 100
577
578        # Beta and Alpha (vs benchmark)
579        beta = np.nan
580        alpha = np.nan
581
582        try:
583            bench = Index(self._benchmark)
584            bench_hist = bench.history(period=period)
585            if not bench_hist.empty:
586                bench_returns = bench_hist["Close"].pct_change().dropna()
587
588                # Align dates
589                common_dates = daily_returns.index.intersection(bench_returns.index)
590                if len(common_dates) >= 20:
591                    port_ret = daily_returns.loc[common_dates]
592                    bench_ret = bench_returns.loc[common_dates]
593
594                    # Beta = Cov(Rp, Rm) / Var(Rm)
595                    covariance = port_ret.cov(bench_ret)
596                    variance = bench_ret.var()
597                    if variance > 0:
598                        beta = covariance / variance
599
600                        # Alpha = Rp - Rf - Beta * (Rm - Rf)
601                        bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1
602                        bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100
603                        alpha = ann_return - rf - beta * (bench_ann - rf)
604        except Exception:
605            pass
606
607        return {
608            "annualized_return": round(ann_return, 2),
609            "annualized_volatility": round(ann_volatility, 2),
610            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
611            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
612            "max_drawdown": round(max_drawdown, 2),
613            "beta": round(beta, 2) if not np.isnan(beta) else np.nan,
614            "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan,
615            "risk_free_rate": round(rf, 2),
616            "trading_days": trading_days,
617        }

Calculate comprehensive risk metrics.

Args: period: Period for calculation (1y, 3mo, 6mo). risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%). If None, uses current 10Y bond yield.

Returns: Dictionary with: - annualized_return: Annualized return (%) - annualized_volatility: Annualized volatility (%) - sharpe_ratio: Risk-adjusted return - sortino_ratio: Downside risk-adjusted return - max_drawdown: Maximum drawdown (%) - beta: Beta vs benchmark - alpha: Alpha vs benchmark (%) - risk_free_rate: Risk-free rate used (%) - trading_days: Number of trading days

def sharpe_ratio(self, period: str = '1y') -> float:
619    def sharpe_ratio(self, period: str = "1y") -> float:
620        """
621        Calculate Sharpe ratio.
622
623        Args:
624            period: Period for calculation.
625
626        Returns:
627            Sharpe ratio.
628        """
629        return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)

Calculate Sharpe ratio.

Args: period: Period for calculation.

Returns: Sharpe ratio.

def sortino_ratio(self, period: str = '1y') -> float:
631    def sortino_ratio(self, period: str = "1y") -> float:
632        """
633        Calculate Sortino ratio.
634
635        Args:
636            period: Period for calculation.
637
638        Returns:
639            Sortino ratio.
640        """
641        return self.risk_metrics(period=period).get("sortino_ratio", np.nan)

Calculate Sortino ratio.

Args: period: Period for calculation.

Returns: Sortino ratio.

def beta(self, benchmark: str | None = None, period: str = '1y') -> float:
643    def beta(self, benchmark: str | None = None, period: str = "1y") -> float:
644        """
645        Calculate beta vs benchmark.
646
647        Args:
648            benchmark: Benchmark index. Uses portfolio default if None.
649            period: Period for calculation.
650
651        Returns:
652            Beta coefficient.
653        """
654        if benchmark:
655            old_bench = self._benchmark
656            self._benchmark = benchmark
657            result = self.risk_metrics(period=period).get("beta", np.nan)
658            self._benchmark = old_bench
659            return result
660        return self.risk_metrics(period=period).get("beta", np.nan)

Calculate beta vs benchmark.

Args: benchmark: Benchmark index. Uses portfolio default if None. period: Period for calculation.

Returns: Beta coefficient.

def correlation_matrix(self, period: str = '1y') -> pandas.core.frame.DataFrame:
662    def correlation_matrix(self, period: str = "1y") -> pd.DataFrame:
663        """
664        Calculate correlation matrix between holdings.
665
666        Args:
667            period: Period for calculation.
668
669        Returns:
670            DataFrame with correlation coefficients.
671        """
672        if len(self._holdings) < 2:
673            return pd.DataFrame()
674
675        returns_dict = {}
676        for symbol, holding in self._holdings.items():
677            try:
678                asset = self._get_or_create_asset(symbol, holding.asset_type)
679                hist = asset.history(period=period)
680                if hist.empty:
681                    continue
682                price_col = "Close" if "Close" in hist.columns else "Price"
683                returns_dict[symbol] = hist[price_col].pct_change()
684            except Exception:
685                continue
686
687        if len(returns_dict) < 2:
688            return pd.DataFrame()
689
690        df = pd.DataFrame(returns_dict).dropna()
691        return df.corr()

Calculate correlation matrix between holdings.

Args: period: Period for calculation.

Returns: DataFrame with correlation coefficients.

def to_dict(self) -> dict[str, typing.Any]:
695    def to_dict(self) -> dict[str, Any]:
696        """
697        Export portfolio to dictionary.
698
699        Returns:
700            Dictionary with portfolio data.
701        """
702        return {
703            "benchmark": self._benchmark,
704            "holdings": [
705                {
706                    "symbol": h.symbol,
707                    "shares": h.shares,
708                    "cost_per_share": h.cost_per_share,
709                    "asset_type": h.asset_type,
710                    "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None,
711                }
712                for h in self._holdings.values()
713            ],
714        }

Export portfolio to dictionary.

Returns: Dictionary with portfolio data.

@classmethod
def from_dict(cls, data: dict[str, typing.Any]) -> Portfolio:
716    @classmethod
717    def from_dict(cls, data: dict[str, Any]) -> "Portfolio":
718        """
719        Create portfolio from dictionary.
720
721        Args:
722            data: Dictionary with portfolio data.
723
724        Returns:
725            Portfolio instance.
726        """
727        portfolio = cls(benchmark=data.get("benchmark", "XU100"))
728        for h in data.get("holdings", []):
729            # Parse purchase_date from ISO string
730            purchase_date = None
731            if h.get("purchase_date"):
732                purchase_date = date.fromisoformat(h["purchase_date"])
733
734            portfolio.add(
735                symbol=h["symbol"],
736                shares=h["shares"],
737                cost=h.get("cost_per_share"),
738                asset_type=h.get("asset_type"),
739                purchase_date=purchase_date,
740            )
741        return portfolio

Create portfolio from dictionary.

Args: data: Dictionary with portfolio data.

Returns: Portfolio instance.

class Index(borsapy.technical.TechnicalMixin):
 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', ...]
Index(symbol: str)
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").

symbol: str
88    @property
89    def symbol(self) -> str:
90        """Return the index symbol."""
91        return self._symbol

Return the index symbol.

info: dict[str, typing.Any]
 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

components: list[dict[str, typing.Any]]
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

component_symbols: list[str]
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', ...]

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame:
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")

def scan( self, condition: str, period: str = '3mo', interval: str = '1d') -> pandas.core.frame.DataFrame:
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")

class Inflation:
 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, ...}
Inflation()
35    def __init__(self):
36        """Initialize an Inflation object."""
37        self._provider = get_tcmb_provider()

Initialize an Inflation object.

def latest(self, inflation_type: str = 'tufe') -> dict[str, typing.Any]:
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)

def tufe( self, start: str | None = None, end: str | None = None, limit: int | None = None) -> pandas.core.frame.DataFrame:
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

def ufe( self, start: str | None = None, end: str | None = None, limit: int | None = None) -> pandas.core.frame.DataFrame:
 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

def calculate(self, amount: float, start: str, end: str) -> dict[str, typing.Any]:
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

class VIOP:
 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

VIOP()
31    def __init__(self) -> None:
32        """Initialize VİOP data accessor."""
33        self._provider = get_viop_provider()

Initialize VİOP data accessor.

futures: pandas.core.frame.DataFrame
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

stock_futures: pandas.core.frame.DataFrame
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.

index_futures: pandas.core.frame.DataFrame
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.

currency_futures: pandas.core.frame.DataFrame
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.

commodity_futures: pandas.core.frame.DataFrame
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.

options: pandas.core.frame.DataFrame
 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

stock_options: pandas.core.frame.DataFrame
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.

index_options: pandas.core.frame.DataFrame
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.

def get_by_symbol(self, symbol: str) -> pandas.core.frame.DataFrame:
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.

class Bond:
 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
Bond(maturity: str)
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).

MATURITIES = ['2Y', '5Y', '10Y']
maturity: str
48    @property
49    def maturity(self) -> str:
50        """Return the bond maturity."""
51        return self._maturity

Return the bond maturity.

name: str
60    @property
61    def name(self) -> str:
62        """Return the bond name."""
63        return self._data.get("name", "")

Return the bond name.

yield_rate: float | None
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%).

yield_decimal: float | None
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.

change: float | None
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.

change_pct: float | None
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.

info: dict[str, typing.Any]
 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.

class Eurobond:
 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'

Eurobond(isin: str)
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.

isin: str
80    @property
81    def isin(self) -> str:
82        """ISIN code of the bond."""
83        return self._data["isin"]

ISIN code of the bond.

maturity: datetime.datetime | None
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.

days_to_maturity: int
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.

currency: str
95    @property
96    def currency(self) -> str:
97        """Bond currency (USD or EUR)."""
98        return self._data.get("currency", "")

Bond currency (USD or EUR).

bid_price: float | None
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).

bid_yield: float | None
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.

ask_price: float | None
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).

ask_yield: float | None
115    @property
116    def ask_yield(self) -> float | None:
117        """Ask yield (selling yield) as percentage."""
118        return self._data.get("ask_yield")

Ask yield (selling yield) as percentage.

info: dict
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()

All bond data as dictionary.

Returns: Dict with all bond attributes.

class TCMB:
 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}

TCMB()
55    def __init__(self):
56        """Initialize TCMB interface."""
57        self._provider = get_tcmb_rates_provider()

Initialize TCMB interface.

policy_rate: float | None
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%).

overnight: dict
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}

late_liquidity: dict
 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}

rates: pandas.core.frame.DataFrame
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

def history( self, rate_type: str = 'policy', period: str | None = None) -> pandas.core.frame.DataFrame:
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 ...

class EconomicCalendar:
 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
EconomicCalendar()
32    def __init__(self):
33        """Initialize EconomicCalendar."""
34        self._provider = get_calendar_provider()

Initialize EconomicCalendar.

COUNTRIES = ['TR', 'US', 'EU', 'DE', 'GB', 'JP', 'CN', 'FR', 'IT', 'CA', 'AU', 'CH']
def events( self, period: str = '1w', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, country: str | list[str] | None = None, importance: str | None = None) -> pandas.core.frame.DataFrame:
 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

def today( self, country: str | list[str] | None = None, importance: str | None = None) -> pandas.core.frame.DataFrame:
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.

def this_week( self, country: str | list[str] | None = None, importance: str | None = None) -> pandas.core.frame.DataFrame:
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.

def this_month( self, country: str | list[str] | None = None, importance: str | None = None) -> pandas.core.frame.DataFrame:
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.

def high_importance( self, period: str = '1w', country: str | list[str] | None = None) -> pandas.core.frame.DataFrame:
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.

@staticmethod
def countries() -> list[str]:
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', ...]

class Screener:
 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)
Screener()
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.

TEMPLATES = ['small_cap', 'mid_cap', 'large_cap', 'high_dividend', 'high_upside', 'low_upside', 'high_volume', 'low_volume', 'buy_recommendation', 'sell_recommendation', 'high_net_margin', 'high_return', 'low_pe', 'high_roe', 'high_foreign_ownership']
CRITERIA_DEFAULTS = {'price': {'min': 0, 'max': 100000}, 'market_cap': {'min': 0, 'max': 5000000}, 'market_cap_usd': {'min': 0, 'max': 100000}, 'pe': {'min': -1000, 'max': 10000}, 'pb': {'min': -100, 'max': 1000}, 'ev_ebitda': {'min': -100, 'max': 1000}, 'ev_sales': {'min': -100, 'max': 1000}, 'dividend_yield': {'min': 0, 'max': 100}, 'dividend_yield_2025': {'min': 0, 'max': 100}, 'roe': {'min': -200, 'max': 500}, 'roa': {'min': -200, 'max': 500}, 'net_margin': {'min': -200, 'max': 500}, 'ebitda_margin': {'min': -200, 'max': 500}, 'upside_potential': {'min': -100, 'max': 500}, 'foreign_ratio': {'min': 0, 'max': 100}, 'float_ratio': {'min': 0, 'max': 100}, 'return_1w': {'min': -100, 'max': 100}, 'return_1m': {'min': -100, 'max': 200}, 'return_1y': {'min': -100, 'max': 1000}, 'return_ytd': {'min': -100, 'max': 1000}, 'volume_3m': {'min': 0, 'max': 1000}, 'volume_12m': {'min': 0, 'max': 1000}, 'float_market_cap': {'min': 0, 'max': 100000}}
def add_filter( self, criteria: str, min: float | None = None, max: float | None = None, required: bool = False) -> 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)

def set_sector(self, sector: str) -> Screener:
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.

def set_index(self, index: str) -> Screener:
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.

def set_recommendation(self, recommendation: str) -> Screener:
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.

def clear(self) -> Screener:
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.

def run(self, template: str | None = None) -> pandas.core.frame.DataFrame:
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.

class TradingViewStream:
 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...
TradingViewStream(auth_token: str | None = None)
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).

WS_URL = 'wss://data.tradingview.com/socket.io/websocket?type=chart'
ORIGIN = 'https://www.tradingview.com'
MAX_RECONNECT_ATTEMPTS = 10
MAX_RECONNECT_DELAY = 30
HEARTBEAT_INTERVAL = 30
is_connected: bool
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.

subscribed_symbols: set[str]
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.

def connect(self, timeout: float = 10.0) -> bool:
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

def disconnect(self) -> None:
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

def subscribe(self, symbol: str, exchange: str = 'BIST') -> None:
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")

def unsubscribe(self, symbol: str, exchange: str = 'BIST') -> None:
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")

def subscribe_chart(self, symbol: str, interval: str = '1m', exchange: str = 'BIST') -> None:
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")

def unsubscribe_chart(self, symbol: str, interval: str, exchange: str = 'BIST') -> None:
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")

def get_candle(self, symbol: str, interval: str) -> dict[str, typing.Any] | None:
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

def get_candles( self, symbol: str, interval: str, count: int | None = None) -> list[dict[str, typing.Any]]:
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

def wait_for_candle( self, symbol: str, interval: str, timeout: float = 5.0) -> dict[str, typing.Any]:
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")

def on_candle( self, symbol: str, interval: str, callback: Callable[[str, str, dict], None]) -> None:
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)

def on_any_candle(self, callback: Callable[[str, str, dict], None]) -> None:
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)

def remove_candle_callback( self, symbol: str, interval: str, callback: Callable[[str, str, dict], None]) -> None:
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

chart_subscriptions: dict[str, set[str]]
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.

def get_quote(self, symbol: str) -> dict[str, typing.Any] | None:
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

def wait_for_quote(self, symbol: str, timeout: float = 5.0) -> dict[str, typing.Any]:
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")

def on_quote(self, symbol: str, callback: Callable[[str, dict], None]) -> None:
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)

def on_any_quote(self, callback: Callable[[str, dict], None]) -> None:
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)

def remove_callback(self, symbol: str, callback: Callable[[str, dict], None]) -> None:
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

def wait(self) -> None:
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

studies: borsapy.stream.StudySession
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")

def add_study(self, symbol: str, interval: str, indicator: str, **kwargs: Any) -> str:
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")

def remove_study(self, symbol: str, interval: str, indicator: str) -> None:
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")

def get_study( self, symbol: str, interval: str, indicator: str) -> dict[str, typing.Any] | None:
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

def get_studies(self, symbol: str, interval: str) -> dict[str, dict[str, typing.Any]]:
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

def on_study( self, symbol: str, interval: str, indicator: str, callback: Callable[[str, str, str, dict], None]) -> None:
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)

def on_any_study(self, callback: Callable[[str, str, str, dict], None]) -> None:
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}") ... )

def wait_for_study( self, symbol: str, interval: str, indicator: str, timeout: float = 10.0) -> dict[str, typing.Any]:
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")

def get_all_quotes(self) -> dict[str, dict[str, typing.Any]]:
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.

def ping(self) -> float:
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.

class ReplaySession:
 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
ReplaySession( symbol: str, df: pandas.core.frame.DataFrame | None = None, speed: float = 1.0, realtime_injection: bool = False)
 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.

symbol
speed
realtime_injection
total_candles: int
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.

progress: float
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).

def set_data(self, df: pandas.core.frame.DataFrame) -> None:
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.

def on_candle(self, callback: Callable[[dict], None]) -> None:
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)

def remove_callback(self, callback: Callable[[dict], None]) -> None:
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.

def replay(self) -> Generator[dict[str, typing.Any], None, None]:
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']}")

def replay_filtered( self, start_date: str | datetime.datetime | None = None, end_date: str | datetime.datetime | None = None) -> Generator[dict[str, typing.Any], None, None]:
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

def stats(self) -> dict[str, typing.Any]:
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, ...}

def reset(self) -> None:
407    def reset(self) -> None:
408        """Reset replay to beginning."""
409        self._current_index = 0
410        self._start_time = None

Reset replay to beginning.

def companies() -> pandas.core.frame.DataFrame:
 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 ...

def search_companies(query: str) -> pandas.core.frame.DataFrame:
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
...
def search_bist(query: str, limit: int = 50) -> list[str]:
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']

def search_crypto(query: str, limit: int = 50) -> list[str]:
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', ...]

def search_forex(query: str, limit: int = 50) -> list[str]:
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', ...]

def search_index(query: str, limit: int = 50) -> list[str]:
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', ...]

def search_viop(query: str, limit: int = 50) -> list[str]:
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', ...]
def viop_contracts(base_symbol: str, full_info: bool = False) -> list[str] | list[dict]:
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

def banks() -> list[str]:
43def banks() -> list[str]:
44    """
45    Get list of supported banks for exchange rates.
46
47    Returns:
48        List of bank codes.
49
50    Examples:
51        >>> import borsapy as bp
52        >>> bp.banks()
53        ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]
54    """
55    return get_dovizcom_provider().get_banks()

Get list of supported banks for exchange rates.

Returns: List of bank codes.

Examples:

import borsapy as bp bp.banks() ['akbank', 'albaraka', 'alternatifbank', 'anadolubank', ...]

def metal_institutions() -> list[str]:
58def metal_institutions() -> list[str]:
59    """
60    Get list of supported precious metal assets for institution rates.
61
62    Returns:
63        List of asset codes that support institution_rates.
64
65    Examples:
66        >>> import borsapy as bp
67        >>> bp.metal_institutions()
68        ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']
69    """
70    return get_dovizcom_provider().get_metal_institutions()

Get list of supported precious metal assets for institution rates.

Returns: List of asset codes that support institution_rates.

Examples:

import borsapy as bp bp.metal_institutions() ['gram-altin', 'gram-gumus', 'gram-platin', 'ons-altin']

def crypto_pairs(quote: str = 'TRY') -> list[str]:
150def crypto_pairs(quote: str = "TRY") -> list[str]:
151    """
152    Get list of available cryptocurrency trading pairs.
153
154    Args:
155        quote: Quote currency filter (TRY, USDT, BTC)
156
157    Returns:
158        List of available trading pair symbols.
159
160    Examples:
161        >>> import borsapy as bp
162        >>> bp.crypto_pairs()
163        ['BTCTRY', 'ETHTRY', 'XRPTRY', ...]
164        >>> bp.crypto_pairs("USDT")
165        ['BTCUSDT', 'ETHUSDT', ...]
166    """
167    provider = get_btcturk_provider()
168    return provider.get_pairs(quote)

Get list of available cryptocurrency trading pairs.

Args: quote: Quote currency filter (TRY, USDT, BTC)

Returns: List of available trading pair symbols.

Examples:

import borsapy as bp bp.crypto_pairs() ['BTCTRY', 'ETHTRY', 'XRPTRY', ...] bp.crypto_pairs("USDT") ['BTCUSDT', 'ETHUSDT', ...]

def search_funds(query: str, limit: int = 20) -> list[dict[str, typing.Any]]:
486def search_funds(query: str, limit: int = 20) -> list[dict[str, Any]]:
487    """
488    Search for funds by name or code.
489
490    Args:
491        query: Search query (fund code or name)
492        limit: Maximum number of results
493
494    Returns:
495        List of matching funds with fund_code, name, fund_type, return_1y.
496
497    Examples:
498        >>> import borsapy as bp
499        >>> bp.search_funds("ak portföy")
500        [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...]
501        >>> bp.search_funds("TTE")
502        [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}]
503    """
504    provider = get_tefas_provider()
505    return provider.search(query, limit)

Search for funds by name or code.

Args: query: Search query (fund code or name) limit: Maximum number of results

Returns: List of matching funds with fund_code, name, fund_type, return_1y.

Examples:

import borsapy as bp bp.search_funds("ak portföy") [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...] bp.search_funds("TTE") [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}]

def screen_funds( fund_type: str = 'YAT', founder: str | None = None, min_return_1m: float | None = None, min_return_3m: float | None = None, min_return_6m: float | None = None, min_return_ytd: float | None = None, min_return_1y: float | None = None, min_return_3y: float | None = None, limit: int = 50) -> pandas.core.frame.DataFrame:
508def screen_funds(
509    fund_type: str = "YAT",
510    founder: str | None = None,
511    min_return_1m: float | None = None,
512    min_return_3m: float | None = None,
513    min_return_6m: float | None = None,
514    min_return_ytd: float | None = None,
515    min_return_1y: float | None = None,
516    min_return_3y: float | None = None,
517    limit: int = 50,
518) -> pd.DataFrame:
519    """
520    Screen funds based on fund type and return criteria.
521
522    Args:
523        fund_type: Fund type filter:
524            - "YAT": Investment Funds (Yatırım Fonları) - default
525            - "EMK": Pension Funds (Emeklilik Fonları)
526        founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP")
527        min_return_1m: Minimum 1-month return (%)
528        min_return_3m: Minimum 3-month return (%)
529        min_return_6m: Minimum 6-month return (%)
530        min_return_ytd: Minimum year-to-date return (%)
531        min_return_1y: Minimum 1-year return (%)
532        min_return_3y: Minimum 3-year return (%)
533        limit: Maximum number of results (default: 50)
534
535    Returns:
536        DataFrame with funds matching the criteria, sorted by 1-year return.
537
538    Examples:
539        >>> import borsapy as bp
540        >>> bp.screen_funds(fund_type="EMK")  # All pension funds
541           fund_code                    name  return_1y  ...
542
543        >>> bp.screen_funds(min_return_1y=50)  # Funds with >50% 1Y return
544           fund_code                    name  return_1y  ...
545
546        >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20)
547           fund_code                    name  return_ytd  ...
548    """
549    provider = get_tefas_provider()
550    results = provider.screen_funds(
551        fund_type=fund_type,
552        founder=founder,
553        min_return_1m=min_return_1m,
554        min_return_3m=min_return_3m,
555        min_return_6m=min_return_6m,
556        min_return_ytd=min_return_ytd,
557        min_return_1y=min_return_1y,
558        min_return_3y=min_return_3y,
559        limit=limit,
560    )
561
562    if not results:
563        return pd.DataFrame(columns=["fund_code", "name", "fund_type", "return_1y"])
564
565    return pd.DataFrame(results)

Screen funds based on fund type and return criteria.

Args: fund_type: Fund type filter: - "YAT": Investment Funds (Yatırım Fonları) - default - "EMK": Pension Funds (Emeklilik Fonları) founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP") min_return_1m: Minimum 1-month return (%) min_return_3m: Minimum 3-month return (%) min_return_6m: Minimum 6-month return (%) min_return_ytd: Minimum year-to-date return (%) min_return_1y: Minimum 1-year return (%) min_return_3y: Minimum 3-year return (%) limit: Maximum number of results (default: 50)

Returns: DataFrame with funds matching the criteria, sorted by 1-year return.

Examples:

import borsapy as bp bp.screen_funds(fund_type="EMK") # All pension funds fund_code name return_1y ...

>>> bp.screen_funds(min_return_1y=50)  # Funds with >50% 1Y return
   fund_code                    name  return_1y  ...

>>> bp.screen_funds(fund_type="EMK", min_return_ytd=20)
   fund_code                    name  return_ytd  ...
def compare_funds(fund_codes: list[str]) -> dict[str, typing.Any]:
568def compare_funds(fund_codes: list[str]) -> dict[str, Any]:
569    """
570    Compare multiple funds side by side.
571
572    Args:
573        fund_codes: List of TEFAS fund codes to compare (max 10)
574
575    Returns:
576        Dictionary with:
577        - funds: List of fund details with performance metrics
578        - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc)
579        - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size)
580
581    Examples:
582        >>> import borsapy as bp
583        >>> result = bp.compare_funds(["AAK", "TTE", "YAF"])
584        >>> result['rankings']['by_return_1y']
585        ['TTE', 'YAF', 'AAK']
586
587        >>> result['summary']
588        {'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...}
589
590        >>> for fund in result['funds']:
591        ...     print(f"{fund['fund_code']}: {fund['return_1y']}%")
592        AAK: 32.5%
593        TTE: 72.1%
594        YAF: 31.0%
595    """
596    provider = get_tefas_provider()
597    return provider.compare_funds(fund_codes)

Compare multiple funds side by side.

Args: fund_codes: List of TEFAS fund codes to compare (max 10)

Returns: Dictionary with: - funds: List of fund details with performance metrics - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc) - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size)

Examples:

import borsapy as bp result = bp.compare_funds(["AAK", "TTE", "YAF"]) result['rankings']['by_return_1y'] ['TTE', 'YAF', 'AAK']

>>> result['summary']
{'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...}

>>> for fund in result['funds']:
...     print(f"{fund['fund_code']}: {fund['return_1y']}%")
AAK: 32.5%
TTE: 72.1%
YAF: 31.0%
def download( tickers: str | list[str], period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, group_by: str = 'column', progress: bool = True) -> pandas.core.frame.DataFrame:
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
def index(symbol: str) -> Index:
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")

def indices(detailed: bool = False) -> list[str] | list[dict[str, typing.Any]]:
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}, ...]

def all_indices() -> list[dict[str, typing.Any]]:
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}, ...]

def bonds() -> pandas.core.frame.DataFrame:
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

def risk_free_rate() -> float | None:
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

def eurobonds(currency: str | None = None) -> pandas.core.frame.DataFrame:
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

def policy_rate() -> float | None:
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

def economic_calendar( period: str = '1w', country: str | list[str] | None = None, importance: str | None = None) -> pandas.core.frame.DataFrame:
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")

def screen_stocks( template: str | None = None, sector: str | None = None, index: str | None = None, recommendation: str | None = None, market_cap_min: float | None = None, market_cap_max: float | None = None, pe_min: float | None = None, pe_max: float | None = None, pb_min: float | None = None, pb_max: float | None = None, dividend_yield_min: float | None = None, dividend_yield_max: float | None = None, upside_potential_min: float | None = None, upside_potential_max: float | None = None, net_margin_min: float | None = None, net_margin_max: float | None = None, roe_min: float | None = None, roe_max: float | None = None) -> pandas.core.frame.DataFrame:
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
... )
def screener_criteria() -> list[dict[str, typing.Any]]:
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'}, ...]

def sectors() -> list[str]:
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', ...]

def stock_indices() -> list[str]:
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', ...]

class TechnicalScanner:
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"

TechnicalScanner()
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.

def set_universe(self, universe: str | list[str]) -> TechnicalScanner:
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

def add_symbol(self, symbol: str) -> TechnicalScanner:
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

def remove_symbol(self, symbol: str) -> TechnicalScanner:
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

def add_condition( self, condition: str, name: str | None = None) -> TechnicalScanner:
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")

def remove_condition(self, name_or_condition: str) -> TechnicalScanner:
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

def clear_conditions(self) -> TechnicalScanner:
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

def set_interval(self, interval: str) -> TechnicalScanner:
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

def add_column(self, column: str) -> TechnicalScanner:
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

def run(self, limit: int = 100) -> pandas.core.frame.DataFrame:
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

symbols: list[str]
352    @property
353    def symbols(self) -> list[str]:
354        """Get current symbol universe."""
355        return self._symbols.copy()

Get current symbol universe.

conditions: list[str]
357    @property
358    def conditions(self) -> list[str]:
359        """Get current conditions."""
360        return self._conditions.copy()

Get current conditions.

def set_data_period(self, period: str = '3mo') -> TechnicalScanner:
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.

results: list[ScanResult]
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.

def to_dataframe(self) -> pandas.core.frame.DataFrame:
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.

def on_match(self, callback) -> None:
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.

def on_scan_complete(self, callback) -> None:
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.

@dataclass
class ScanResult:
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

ScanResult( symbol: str, data: dict[str, typing.Any] = <factory>, conditions_met: list[str] = <factory>, timestamp: datetime.datetime = <factory>)
symbol: str
data: dict[str, typing.Any]
conditions_met: list[str]
timestamp: datetime.datetime
def scan( universe: str | list[str], condition: str, interval: str = '1d', limit: int = 100) -> pandas.core.frame.DataFrame:
 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

class TechnicalAnalyzer:
655class TechnicalAnalyzer:
656    """Technical analysis wrapper for OHLCV DataFrames.
657
658    Provides easy access to technical indicators as methods and properties.
659
660    Example:
661        >>> df = stock.history(period="1y")
662        >>> ta = TechnicalAnalyzer(df)
663        >>> ta.rsi()  # Returns full RSI series
664        >>> ta.latest  # Returns dict with latest values of all indicators
665    """
666
667    def __init__(self, df: pd.DataFrame) -> None:
668        """Initialize with OHLCV DataFrame.
669
670        Args:
671            df: DataFrame with price data (must have at least 'Close' column)
672        """
673        self._df = df.copy()
674        self._has_volume = "Volume" in df.columns
675        self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])
676
677    def sma(self, period: int = 20) -> pd.Series:
678        """Calculate Simple Moving Average."""
679        return calculate_sma(self._df, period)
680
681    def ema(self, period: int = 20) -> pd.Series:
682        """Calculate Exponential Moving Average."""
683        return calculate_ema(self._df, period)
684
685    def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series:
686        """Calculate Tilson T3 Moving Average."""
687        return calculate_tilson_t3(self._df, period, vfactor)
688
689    def rsi(self, period: int = 14) -> pd.Series:
690        """Calculate Relative Strength Index."""
691        return calculate_rsi(self._df, period)
692
693    def macd(
694        self, fast: int = 12, slow: int = 26, signal: int = 9
695    ) -> pd.DataFrame:
696        """Calculate MACD (line, signal, histogram)."""
697        return calculate_macd(self._df, fast, slow, signal)
698
699    def bollinger_bands(
700        self, period: int = 20, std_dev: float = 2.0
701    ) -> pd.DataFrame:
702        """Calculate Bollinger Bands (upper, middle, lower)."""
703        return calculate_bollinger_bands(self._df, period, std_dev)
704
705    def atr(self, period: int = 14) -> pd.Series:
706        """Calculate Average True Range."""
707        return calculate_atr(self._df, period)
708
709    def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame:
710        """Calculate Stochastic Oscillator (%K, %D)."""
711        return calculate_stochastic(self._df, k_period, d_period)
712
713    def obv(self) -> pd.Series:
714        """Calculate On-Balance Volume."""
715        return calculate_obv(self._df)
716
717    def vwap(self) -> pd.Series:
718        """Calculate Volume Weighted Average Price."""
719        return calculate_vwap(self._df)
720
721    def adx(self, period: int = 14) -> pd.Series:
722        """Calculate Average Directional Index."""
723        return calculate_adx(self._df, period)
724
725    def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame:
726        """Calculate Supertrend indicator.
727
728        Args:
729            atr_period: Period for ATR calculation (default 10)
730            multiplier: ATR multiplier for bands (default 3.0)
731
732        Returns:
733            DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower
734        """
735        return calculate_supertrend(self._df, atr_period, multiplier)
736
737    def heikin_ashi(self) -> pd.DataFrame:
738        """Calculate Heikin Ashi candlestick values.
739
740        Returns:
741            DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
742        """
743        from borsapy.charts import calculate_heikin_ashi
744
745        return calculate_heikin_ashi(self._df)
746
747    def all(self, **kwargs: Any) -> pd.DataFrame:
748        """Get DataFrame with all applicable indicators added."""
749        return add_indicators(self._df, **kwargs)
750
751    @property
752    def latest(self) -> dict[str, float]:
753        """Get latest values of all applicable indicators.
754
755        Returns:
756            Dictionary with indicator names and their latest values
757        """
758        result: dict[str, float] = {}
759
760        # Always available (need Close or Price)
761        has_price = "Close" in self._df.columns or "Price" in self._df.columns
762        if has_price and len(self._df) > 0:
763            result["sma_20"] = float(self.sma(20).iloc[-1])
764            result["sma_50"] = float(self.sma(50).iloc[-1])
765            result["ema_12"] = float(self.ema(12).iloc[-1])
766            result["ema_26"] = float(self.ema(26).iloc[-1])
767            result["t3_5"] = float(self.tilson_t3(5).iloc[-1])
768            result["rsi_14"] = float(self.rsi(14).iloc[-1])
769
770            macd_df = self.macd()
771            result["macd"] = float(macd_df["MACD"].iloc[-1])
772            result["macd_signal"] = float(macd_df["Signal"].iloc[-1])
773            result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1])
774
775            bb_df = self.bollinger_bands()
776            result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1])
777            result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1])
778            result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1])
779
780        # Need High, Low, Close
781        if self._has_hlc and len(self._df) > 0:
782            result["atr_14"] = float(self.atr(14).iloc[-1])
783            result["adx_14"] = float(self.adx(14).iloc[-1])
784
785            stoch_df = self.stochastic()
786            result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1])
787            result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1])
788
789            st_df = self.supertrend()
790            result["supertrend"] = float(st_df["Supertrend"].iloc[-1])
791            result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1])
792
793        # Need Volume
794        if self._has_volume and len(self._df) > 0:
795            result["obv"] = float(self.obv().iloc[-1])
796
797        # Need HLC + Volume
798        if self._has_hlc and self._has_volume and len(self._df) > 0:
799            result["vwap"] = float(self.vwap().iloc[-1])
800
801        # Round all values
802        return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}

Technical analysis wrapper for OHLCV DataFrames.

Provides easy access to technical indicators as methods and properties.

Example:

df = stock.history(period="1y") ta = TechnicalAnalyzer(df) ta.rsi() # Returns full RSI series ta.latest # Returns dict with latest values of all indicators

TechnicalAnalyzer(df: pandas.core.frame.DataFrame)
667    def __init__(self, df: pd.DataFrame) -> None:
668        """Initialize with OHLCV DataFrame.
669
670        Args:
671            df: DataFrame with price data (must have at least 'Close' column)
672        """
673        self._df = df.copy()
674        self._has_volume = "Volume" in df.columns
675        self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])

Initialize with OHLCV DataFrame.

Args: df: DataFrame with price data (must have at least 'Close' column)

def sma(self, period: int = 20) -> pandas.core.series.Series:
677    def sma(self, period: int = 20) -> pd.Series:
678        """Calculate Simple Moving Average."""
679        return calculate_sma(self._df, period)

Calculate Simple Moving Average.

def ema(self, period: int = 20) -> pandas.core.series.Series:
681    def ema(self, period: int = 20) -> pd.Series:
682        """Calculate Exponential Moving Average."""
683        return calculate_ema(self._df, period)

Calculate Exponential Moving Average.

def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pandas.core.series.Series:
685    def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series:
686        """Calculate Tilson T3 Moving Average."""
687        return calculate_tilson_t3(self._df, period, vfactor)

Calculate Tilson T3 Moving Average.

def rsi(self, period: int = 14) -> pandas.core.series.Series:
689    def rsi(self, period: int = 14) -> pd.Series:
690        """Calculate Relative Strength Index."""
691        return calculate_rsi(self._df, period)

Calculate Relative Strength Index.

def macd( self, fast: int = 12, slow: int = 26, signal: int = 9) -> pandas.core.frame.DataFrame:
693    def macd(
694        self, fast: int = 12, slow: int = 26, signal: int = 9
695    ) -> pd.DataFrame:
696        """Calculate MACD (line, signal, histogram)."""
697        return calculate_macd(self._df, fast, slow, signal)

Calculate MACD (line, signal, histogram).

def bollinger_bands( self, period: int = 20, std_dev: float = 2.0) -> pandas.core.frame.DataFrame:
699    def bollinger_bands(
700        self, period: int = 20, std_dev: float = 2.0
701    ) -> pd.DataFrame:
702        """Calculate Bollinger Bands (upper, middle, lower)."""
703        return calculate_bollinger_bands(self._df, period, std_dev)

Calculate Bollinger Bands (upper, middle, lower).

def atr(self, period: int = 14) -> pandas.core.series.Series:
705    def atr(self, period: int = 14) -> pd.Series:
706        """Calculate Average True Range."""
707        return calculate_atr(self._df, period)

Calculate Average True Range.

def stochastic( self, k_period: int = 14, d_period: int = 3) -> pandas.core.frame.DataFrame:
709    def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame:
710        """Calculate Stochastic Oscillator (%K, %D)."""
711        return calculate_stochastic(self._df, k_period, d_period)

Calculate Stochastic Oscillator (%K, %D).

def obv(self) -> pandas.core.series.Series:
713    def obv(self) -> pd.Series:
714        """Calculate On-Balance Volume."""
715        return calculate_obv(self._df)

Calculate On-Balance Volume.

def vwap(self) -> pandas.core.series.Series:
717    def vwap(self) -> pd.Series:
718        """Calculate Volume Weighted Average Price."""
719        return calculate_vwap(self._df)

Calculate Volume Weighted Average Price.

def adx(self, period: int = 14) -> pandas.core.series.Series:
721    def adx(self, period: int = 14) -> pd.Series:
722        """Calculate Average Directional Index."""
723        return calculate_adx(self._df, period)

Calculate Average Directional Index.

def supertrend( self, atr_period: int = 10, multiplier: float = 3.0) -> pandas.core.frame.DataFrame:
725    def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame:
726        """Calculate Supertrend indicator.
727
728        Args:
729            atr_period: Period for ATR calculation (default 10)
730            multiplier: ATR multiplier for bands (default 3.0)
731
732        Returns:
733            DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower
734        """
735        return calculate_supertrend(self._df, atr_period, multiplier)

Calculate Supertrend indicator.

Args: atr_period: Period for ATR calculation (default 10) multiplier: ATR multiplier for bands (default 3.0)

Returns: DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower

def heikin_ashi(self) -> pandas.core.frame.DataFrame:
737    def heikin_ashi(self) -> pd.DataFrame:
738        """Calculate Heikin Ashi candlestick values.
739
740        Returns:
741            DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
742        """
743        from borsapy.charts import calculate_heikin_ashi
744
745        return calculate_heikin_ashi(self._df)

Calculate Heikin Ashi candlestick values.

Returns: DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns

def all(self, **kwargs: Any) -> pandas.core.frame.DataFrame:
747    def all(self, **kwargs: Any) -> pd.DataFrame:
748        """Get DataFrame with all applicable indicators added."""
749        return add_indicators(self._df, **kwargs)

Get DataFrame with all applicable indicators added.

latest: dict[str, float]
751    @property
752    def latest(self) -> dict[str, float]:
753        """Get latest values of all applicable indicators.
754
755        Returns:
756            Dictionary with indicator names and their latest values
757        """
758        result: dict[str, float] = {}
759
760        # Always available (need Close or Price)
761        has_price = "Close" in self._df.columns or "Price" in self._df.columns
762        if has_price and len(self._df) > 0:
763            result["sma_20"] = float(self.sma(20).iloc[-1])
764            result["sma_50"] = float(self.sma(50).iloc[-1])
765            result["ema_12"] = float(self.ema(12).iloc[-1])
766            result["ema_26"] = float(self.ema(26).iloc[-1])
767            result["t3_5"] = float(self.tilson_t3(5).iloc[-1])
768            result["rsi_14"] = float(self.rsi(14).iloc[-1])
769
770            macd_df = self.macd()
771            result["macd"] = float(macd_df["MACD"].iloc[-1])
772            result["macd_signal"] = float(macd_df["Signal"].iloc[-1])
773            result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1])
774
775            bb_df = self.bollinger_bands()
776            result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1])
777            result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1])
778            result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1])
779
780        # Need High, Low, Close
781        if self._has_hlc and len(self._df) > 0:
782            result["atr_14"] = float(self.atr(14).iloc[-1])
783            result["adx_14"] = float(self.adx(14).iloc[-1])
784
785            stoch_df = self.stochastic()
786            result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1])
787            result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1])
788
789            st_df = self.supertrend()
790            result["supertrend"] = float(st_df["Supertrend"].iloc[-1])
791            result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1])
792
793        # Need Volume
794        if self._has_volume and len(self._df) > 0:
795            result["obv"] = float(self.obv().iloc[-1])
796
797        # Need HLC + Volume
798        if self._has_hlc and self._has_volume and len(self._df) > 0:
799            result["vwap"] = float(self.vwap().iloc[-1])
800
801        # Round all values
802        return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}

Get latest values of all applicable indicators.

Returns: Dictionary with indicator names and their latest values

def add_indicators( df: pandas.core.frame.DataFrame, indicators: list[str] | None = None, **kwargs: Any) -> pandas.core.frame.DataFrame:
560def add_indicators(
561    df: pd.DataFrame,
562    indicators: list[str] | None = None,
563    **kwargs: Any,
564) -> pd.DataFrame:
565    """Add technical indicator columns to a DataFrame.
566
567    Args:
568        df: DataFrame with OHLCV data (Open, High, Low, Close, Volume)
569        indicators: List of indicators to add. If None, adds all applicable.
570            Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr',
571                     'stochastic', 'obv', 'vwap', 'adx', 'supertrend'
572        **kwargs: Additional arguments for specific indicators:
573            - sma_period: SMA period (default 20)
574            - ema_period: EMA period (default 12)
575            - rsi_period: RSI period (default 14)
576            - bb_period: Bollinger Bands period (default 20)
577            - atr_period: ATR period (default 14)
578            - adx_period: ADX period (default 14)
579            - supertrend_period: Supertrend ATR period (default 10)
580            - supertrend_multiplier: Supertrend ATR multiplier (default 3.0)
581
582    Returns:
583        DataFrame with indicator columns added
584    """
585    result = df.copy()
586
587    # Default indicators based on available columns
588    has_volume = "Volume" in df.columns
589    has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])
590
591    if indicators is None:
592        indicators = ["sma", "ema", "rsi", "macd", "bollinger"]
593        if has_hlc:
594            indicators.extend(["atr", "stochastic", "adx", "supertrend"])
595        if has_volume:
596            indicators.append("obv")
597        if has_volume and has_hlc:
598            indicators.append("vwap")
599
600    # Get periods from kwargs
601    sma_period = kwargs.get("sma_period", 20)
602    ema_period = kwargs.get("ema_period", 12)
603    rsi_period = kwargs.get("rsi_period", 14)
604    bb_period = kwargs.get("bb_period", 20)
605    atr_period = kwargs.get("atr_period", 14)
606    adx_period = kwargs.get("adx_period", 14)
607    supertrend_period = kwargs.get("supertrend_period", 10)
608    supertrend_multiplier = kwargs.get("supertrend_multiplier", 3.0)
609
610    # Add indicators
611    for indicator in indicators:
612        indicator = indicator.lower()
613
614        if indicator == "sma":
615            result[f"SMA_{sma_period}"] = calculate_sma(df, sma_period)
616        elif indicator == "ema":
617            result[f"EMA_{ema_period}"] = calculate_ema(df, ema_period)
618        elif indicator == "rsi":
619            result[f"RSI_{rsi_period}"] = calculate_rsi(df, rsi_period)
620        elif indicator == "macd":
621            macd_df = calculate_macd(df)
622            result["MACD"] = macd_df["MACD"]
623            result["MACD_Signal"] = macd_df["Signal"]
624            result["MACD_Hist"] = macd_df["Histogram"]
625        elif indicator == "bollinger":
626            bb_df = calculate_bollinger_bands(df, bb_period)
627            result["BB_Upper"] = bb_df["BB_Upper"]
628            result["BB_Middle"] = bb_df["BB_Middle"]
629            result["BB_Lower"] = bb_df["BB_Lower"]
630        elif indicator == "atr" and has_hlc:
631            result[f"ATR_{atr_period}"] = calculate_atr(df, atr_period)
632        elif indicator == "stochastic" and has_hlc:
633            stoch_df = calculate_stochastic(df)
634            result["Stoch_K"] = stoch_df["Stoch_K"]
635            result["Stoch_D"] = stoch_df["Stoch_D"]
636        elif indicator == "obv" and has_volume:
637            result["OBV"] = calculate_obv(df)
638        elif indicator == "vwap" and has_volume and has_hlc:
639            result["VWAP"] = calculate_vwap(df)
640        elif indicator == "adx" and has_hlc:
641            result[f"ADX_{adx_period}"] = calculate_adx(df, adx_period)
642        elif indicator == "supertrend" and has_hlc:
643            st_df = calculate_supertrend(df, supertrend_period, supertrend_multiplier)
644            result["Supertrend"] = st_df["Supertrend"]
645            result["Supertrend_Direction"] = st_df["Supertrend_Direction"]
646
647    return result

Add technical indicator columns to a DataFrame.

Args: df: DataFrame with OHLCV data (Open, High, Low, Close, Volume) indicators: List of indicators to add. If None, adds all applicable. Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr', 'stochastic', 'obv', 'vwap', 'adx', 'supertrend' **kwargs: Additional arguments for specific indicators: - sma_period: SMA period (default 20) - ema_period: EMA period (default 12) - rsi_period: RSI period (default 14) - bb_period: Bollinger Bands period (default 20) - atr_period: ATR period (default 14) - adx_period: ADX period (default 14) - supertrend_period: Supertrend ATR period (default 10) - supertrend_multiplier: Supertrend ATR multiplier (default 3.0)

Returns: DataFrame with indicator columns added

def calculate_sma( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series:
55def calculate_sma(
56    df: pd.DataFrame, period: int = 20, column: str = "Close"
57) -> pd.Series:
58    """Calculate Simple Moving Average (SMA).
59
60    Args:
61        df: DataFrame with price data
62        period: Number of periods for moving average
63        column: Column name to use for calculation
64
65    Returns:
66        Series with SMA values
67    """
68    col = _get_price_column(df, column)
69    if col not in df.columns:
70        return pd.Series(np.nan, index=df.index, name=f"SMA_{period}")
71    return df[col].rolling(window=period, min_periods=1).mean()

Calculate Simple Moving Average (SMA).

Args: df: DataFrame with price data period: Number of periods for moving average column: Column name to use for calculation

Returns: Series with SMA values

def calculate_ema( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series:
74def calculate_ema(
75    df: pd.DataFrame, period: int = 20, column: str = "Close"
76) -> pd.Series:
77    """Calculate Exponential Moving Average (EMA).
78
79    Args:
80        df: DataFrame with price data
81        period: Number of periods for moving average
82        column: Column name to use for calculation
83
84    Returns:
85        Series with EMA values
86    """
87    col = _get_price_column(df, column)
88    if col not in df.columns:
89        return pd.Series(np.nan, index=df.index, name=f"EMA_{period}")
90    return df[col].ewm(span=period, adjust=False).mean()

Calculate Exponential Moving Average (EMA).

Args: df: DataFrame with price data period: Number of periods for moving average column: Column name to use for calculation

Returns: Series with EMA values

def calculate_rsi( df: pandas.core.frame.DataFrame, period: int = 14, column: str = 'Close') -> pandas.core.series.Series:
150def calculate_rsi(
151    df: pd.DataFrame, period: int = 14, column: str = "Close"
152) -> pd.Series:
153    """Calculate Relative Strength Index (RSI).
154
155    RSI measures the speed and magnitude of price movements on a scale of 0-100.
156    - RSI > 70: Overbought (potential sell signal)
157    - RSI < 30: Oversold (potential buy signal)
158
159    Args:
160        df: DataFrame with price data
161        period: Number of periods for RSI calculation (default 14)
162        column: Column name to use for calculation
163
164    Returns:
165        Series with RSI values (0-100)
166    """
167    col = _get_price_column(df, column)
168    if col not in df.columns or len(df) < period:
169        return pd.Series(np.nan, index=df.index, name=f"RSI_{period}")
170
171    delta = df[col].diff()
172    gain = delta.where(delta > 0, 0.0)
173    loss = (-delta).where(delta < 0, 0.0)
174
175    # Use Wilder's smoothing (same as TradingView)
176    # Wilder's uses alpha=1/period, NOT span=period
177    avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean()
178    avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean()
179
180    rs = avg_gain / avg_loss
181    rsi = 100.0 - (100.0 / (1.0 + rs))
182
183    # Handle division by zero
184    rsi = rsi.replace([np.inf, -np.inf], np.nan)
185    rsi = rsi.fillna(50.0)  # Neutral RSI when no movement
186
187    return rsi.rename(f"RSI_{period}")

Calculate Relative Strength Index (RSI).

RSI measures the speed and magnitude of price movements on a scale of 0-100.

  • RSI > 70: Overbought (potential sell signal)
  • RSI < 30: Oversold (potential buy signal)

Args: df: DataFrame with price data period: Number of periods for RSI calculation (default 14) column: Column name to use for calculation

Returns: Series with RSI values (0-100)

def calculate_macd( df: pandas.core.frame.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9, column: str = 'Close') -> pandas.core.frame.DataFrame:
190def calculate_macd(
191    df: pd.DataFrame,
192    fast: int = 12,
193    slow: int = 26,
194    signal: int = 9,
195    column: str = "Close",
196) -> pd.DataFrame:
197    """Calculate Moving Average Convergence Divergence (MACD).
198
199    MACD shows the relationship between two moving averages of prices.
200    - MACD Line: Fast EMA - Slow EMA
201    - Signal Line: EMA of MACD Line
202    - Histogram: MACD Line - Signal Line
203
204    Args:
205        df: DataFrame with price data
206        fast: Fast EMA period (default 12)
207        slow: Slow EMA period (default 26)
208        signal: Signal line EMA period (default 9)
209        column: Column name to use for calculation
210
211    Returns:
212        DataFrame with columns: MACD, Signal, Histogram
213    """
214    col = _get_price_column(df, column)
215    if col not in df.columns:
216        return pd.DataFrame(
217            {"MACD": np.nan, "Signal": np.nan, "Histogram": np.nan},
218            index=df.index,
219        )
220
221    ema_fast = df[col].ewm(span=fast, adjust=False).mean()
222    ema_slow = df[col].ewm(span=slow, adjust=False).mean()
223
224    macd_line = ema_fast - ema_slow
225    signal_line = macd_line.ewm(span=signal, adjust=False).mean()
226    histogram = macd_line - signal_line
227
228    return pd.DataFrame(
229        {"MACD": macd_line, "Signal": signal_line, "Histogram": histogram},
230        index=df.index,
231    )

Calculate Moving Average Convergence Divergence (MACD).

MACD shows the relationship between two moving averages of prices.

  • MACD Line: Fast EMA - Slow EMA
  • Signal Line: EMA of MACD Line
  • Histogram: MACD Line - Signal Line

Args: df: DataFrame with price data fast: Fast EMA period (default 12) slow: Slow EMA period (default 26) signal: Signal line EMA period (default 9) column: Column name to use for calculation

Returns: DataFrame with columns: MACD, Signal, Histogram

def calculate_bollinger_bands( df: pandas.core.frame.DataFrame, period: int = 20, std_dev: float = 2.0, column: str = 'Close') -> pandas.core.frame.DataFrame:
234def calculate_bollinger_bands(
235    df: pd.DataFrame, period: int = 20, std_dev: float = 2.0, column: str = "Close"
236) -> pd.DataFrame:
237    """Calculate Bollinger Bands.
238
239    Bollinger Bands consist of a middle band (SMA) and two outer bands
240    at standard deviation levels above and below the middle band.
241
242    Args:
243        df: DataFrame with price data
244        period: Period for SMA and standard deviation
245        std_dev: Number of standard deviations for bands
246        column: Column name to use for calculation
247
248    Returns:
249        DataFrame with columns: Upper, Middle, Lower
250    """
251    col = _get_price_column(df, column)
252    if col not in df.columns:
253        return pd.DataFrame(
254            {"BB_Upper": np.nan, "BB_Middle": np.nan, "BB_Lower": np.nan},
255            index=df.index,
256        )
257
258    middle = df[col].rolling(window=period, min_periods=1).mean()
259    std = df[col].rolling(window=period, min_periods=1).std()
260
261    upper = middle + (std * std_dev)
262    lower = middle - (std * std_dev)
263
264    return pd.DataFrame(
265        {"BB_Upper": upper, "BB_Middle": middle, "BB_Lower": lower},
266        index=df.index,
267    )

Calculate Bollinger Bands.

Bollinger Bands consist of a middle band (SMA) and two outer bands at standard deviation levels above and below the middle band.

Args: df: DataFrame with price data period: Period for SMA and standard deviation std_dev: Number of standard deviations for bands column: Column name to use for calculation

Returns: DataFrame with columns: Upper, Middle, Lower

def calculate_atr( df: pandas.core.frame.DataFrame, period: int = 14) -> pandas.core.series.Series:
270def calculate_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
271    """Calculate Average True Range (ATR).
272
273    ATR measures market volatility by decomposing the entire range of an asset
274    price for that period.
275
276    Args:
277        df: DataFrame with High, Low, Close columns
278        period: Period for ATR calculation
279
280    Returns:
281        Series with ATR values
282    """
283    required = ["High", "Low", "Close"]
284    if not all(col in df.columns for col in required):
285        return pd.Series(np.nan, index=df.index, name=f"ATR_{period}")
286
287    high = df["High"]
288    low = df["Low"]
289    close = df["Close"]
290
291    # True Range components
292    tr1 = high - low
293    tr2 = abs(high - close.shift(1))
294    tr3 = abs(low - close.shift(1))
295
296    # True Range is the maximum of the three
297    tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
298
299    # ATR uses Wilder's smoothing (same as TradingView)
300    atr = tr.ewm(alpha=1 / period, adjust=False).mean()
301
302    return atr.rename(f"ATR_{period}")

Calculate Average True Range (ATR).

ATR measures market volatility by decomposing the entire range of an asset price for that period.

Args: df: DataFrame with High, Low, Close columns period: Period for ATR calculation

Returns: Series with ATR values

def calculate_stochastic( df: pandas.core.frame.DataFrame, k_period: int = 14, d_period: int = 3) -> pandas.core.frame.DataFrame:
305def calculate_stochastic(
306    df: pd.DataFrame, k_period: int = 14, d_period: int = 3
307) -> pd.DataFrame:
308    """Calculate Stochastic Oscillator (%K and %D).
309
310    The Stochastic Oscillator compares a closing price to a range of prices
311    over a certain period of time.
312    - %K > 80: Overbought
313    - %K < 20: Oversold
314
315    Args:
316        df: DataFrame with High, Low, Close columns
317        k_period: Period for %K calculation
318        d_period: Period for %D (signal line)
319
320    Returns:
321        DataFrame with columns: Stoch_K, Stoch_D
322    """
323    required = ["High", "Low", "Close"]
324    if not all(col in df.columns for col in required):
325        return pd.DataFrame(
326            {"Stoch_K": np.nan, "Stoch_D": np.nan},
327            index=df.index,
328        )
329
330    # Calculate %K
331    lowest_low = df["Low"].rolling(window=k_period, min_periods=1).min()
332    highest_high = df["High"].rolling(window=k_period, min_periods=1).max()
333
334    stoch_k = 100 * (df["Close"] - lowest_low) / (highest_high - lowest_low)
335    stoch_k = stoch_k.replace([np.inf, -np.inf], np.nan).fillna(50.0)
336
337    # %D is the SMA of %K
338    stoch_d = stoch_k.rolling(window=d_period, min_periods=1).mean()
339
340    return pd.DataFrame(
341        {"Stoch_K": stoch_k, "Stoch_D": stoch_d},
342        index=df.index,
343    )

Calculate Stochastic Oscillator (%K and %D).

The Stochastic Oscillator compares a closing price to a range of prices over a certain period of time.

  • %K > 80: Overbought
  • %K < 20: Oversold

Args: df: DataFrame with High, Low, Close columns k_period: Period for %K calculation d_period: Period for %D (signal line)

Returns: DataFrame with columns: Stoch_K, Stoch_D

def calculate_obv(df: pandas.core.frame.DataFrame) -> pandas.core.series.Series:
346def calculate_obv(df: pd.DataFrame) -> pd.Series:
347    """Calculate On-Balance Volume (OBV).
348
349    OBV uses volume flow to predict changes in stock price.
350    Rising OBV indicates positive volume pressure that can lead to higher prices.
351
352    Args:
353        df: DataFrame with Close and Volume columns
354
355    Returns:
356        Series with OBV values
357    """
358    required = ["Close", "Volume"]
359    if not all(col in df.columns for col in required):
360        return pd.Series(np.nan, index=df.index, name="OBV")
361
362    # Direction: +1 if close > previous close, -1 if close < previous close, 0 if equal
363    direction = np.sign(df["Close"].diff())
364    direction.iloc[0] = 0  # First value has no direction
365
366    # OBV is cumulative sum of signed volume
367    obv = (direction * df["Volume"]).cumsum()
368
369    return obv.rename("OBV")

Calculate On-Balance Volume (OBV).

OBV uses volume flow to predict changes in stock price. Rising OBV indicates positive volume pressure that can lead to higher prices.

Args: df: DataFrame with Close and Volume columns

Returns: Series with OBV values

def calculate_vwap(df: pandas.core.frame.DataFrame) -> pandas.core.series.Series:
372def calculate_vwap(df: pd.DataFrame) -> pd.Series:
373    """Calculate Volume Weighted Average Price (VWAP).
374
375    VWAP gives the average price weighted by volume.
376    It's often used as a trading benchmark.
377
378    Args:
379        df: DataFrame with High, Low, Close, Volume columns
380
381    Returns:
382        Series with VWAP values
383    """
384    required = ["High", "Low", "Close", "Volume"]
385    if not all(col in df.columns for col in required):
386        return pd.Series(np.nan, index=df.index, name="VWAP")
387
388    # Typical Price
389    typical_price = (df["High"] + df["Low"] + df["Close"]) / 3
390
391    # Cumulative TP * Volume / Cumulative Volume
392    cumulative_tp_vol = (typical_price * df["Volume"]).cumsum()
393    cumulative_vol = df["Volume"].cumsum()
394
395    vwap = cumulative_tp_vol / cumulative_vol
396    vwap = vwap.replace([np.inf, -np.inf], np.nan)
397
398    return vwap.rename("VWAP")

Calculate Volume Weighted Average Price (VWAP).

VWAP gives the average price weighted by volume. It's often used as a trading benchmark.

Args: df: DataFrame with High, Low, Close, Volume columns

Returns: Series with VWAP values

def calculate_adx( df: pandas.core.frame.DataFrame, period: int = 14) -> pandas.core.series.Series:
401def calculate_adx(df: pd.DataFrame, period: int = 14) -> pd.Series:
402    """Calculate Average Directional Index (ADX).
403
404    ADX measures the strength of a trend regardless of its direction.
405    - ADX > 25: Strong trend
406    - ADX < 20: Weak or no trend
407
408    Args:
409        df: DataFrame with High, Low, Close columns
410        period: Period for ADX calculation
411
412    Returns:
413        Series with ADX values
414    """
415    required = ["High", "Low", "Close"]
416    if not all(col in df.columns for col in required):
417        return pd.Series(np.nan, index=df.index, name=f"ADX_{period}")
418
419    high = df["High"]
420    low = df["Low"]
421    close = df["Close"]
422
423    # Calculate +DM and -DM
424    plus_dm = high.diff()
425    minus_dm = -low.diff()
426
427    plus_dm = plus_dm.where((plus_dm > minus_dm) & (plus_dm > 0), 0.0)
428    minus_dm = minus_dm.where((minus_dm > plus_dm) & (minus_dm > 0), 0.0)
429
430    # True Range
431    tr1 = high - low
432    tr2 = abs(high - close.shift(1))
433    tr3 = abs(low - close.shift(1))
434    tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
435
436    # Smoothed values using Wilder's smoothing (same as TradingView)
437    atr = tr.ewm(alpha=1 / period, adjust=False).mean()
438    plus_di = 100 * (plus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr)
439    minus_di = 100 * (minus_dm.ewm(alpha=1 / period, adjust=False).mean() / atr)
440
441    # DX and ADX
442    dx = 100 * abs(plus_di - minus_di) / (plus_di + minus_di)
443    dx = dx.replace([np.inf, -np.inf], np.nan).fillna(0)
444
445    adx = dx.ewm(alpha=1 / period, adjust=False).mean()
446
447    return adx.rename(f"ADX_{period}")

Calculate Average Directional Index (ADX).

ADX measures the strength of a trend regardless of its direction.

  • ADX > 25: Strong trend
  • ADX < 20: Weak or no trend

Args: df: DataFrame with High, Low, Close columns period: Period for ADX calculation

Returns: Series with ADX values

def calculate_supertrend( df: pandas.core.frame.DataFrame, atr_period: int = 10, multiplier: float = 3.0) -> pandas.core.frame.DataFrame:
450def calculate_supertrend(
451    df: pd.DataFrame, atr_period: int = 10, multiplier: float = 3.0
452) -> pd.DataFrame:
453    """Calculate Supertrend indicator.
454
455    Supertrend is a trend-following indicator based on ATR.
456    - When price is above Supertrend line: Bullish (uptrend)
457    - When price is below Supertrend line: Bearish (downtrend)
458
459    Args:
460        df: DataFrame with High, Low, Close columns
461        atr_period: Period for ATR calculation (default: 10)
462        multiplier: ATR multiplier for bands (default: 3.0)
463
464    Returns:
465        DataFrame with columns:
466        - Supertrend: The Supertrend line value
467        - Supertrend_Direction: 1 for bullish, -1 for bearish
468        - Supertrend_Upper: Upper band
469        - Supertrend_Lower: Lower band
470    """
471    required = ["High", "Low", "Close"]
472    if not all(col in df.columns for col in required):
473        return pd.DataFrame(
474            {
475                "Supertrend": np.nan,
476                "Supertrend_Direction": np.nan,
477                "Supertrend_Upper": np.nan,
478                "Supertrend_Lower": np.nan,
479            },
480            index=df.index,
481        )
482
483    high = df["High"]
484    low = df["Low"]
485    close = df["Close"]
486
487    # Calculate ATR
488    tr1 = high - low
489    tr2 = abs(high - close.shift(1))
490    tr3 = abs(low - close.shift(1))
491    tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
492    atr = tr.ewm(alpha=1 / atr_period, adjust=False).mean()
493
494    # Calculate basic bands
495    hl2 = (high + low) / 2
496    basic_upper = hl2 + (multiplier * atr)
497    basic_lower = hl2 - (multiplier * atr)
498
499    # Initialize arrays
500    n = len(df)
501    supertrend = np.zeros(n)
502    direction = np.zeros(n)
503    final_upper = np.zeros(n)
504    final_lower = np.zeros(n)
505
506    # First value
507    final_upper[0] = basic_upper.iloc[0]
508    final_lower[0] = basic_lower.iloc[0]
509    supertrend[0] = basic_upper.iloc[0]
510    direction[0] = -1  # Start bearish
511
512    # Calculate Supertrend
513    for i in range(1, n):
514        # Final Upper Band
515        if basic_upper.iloc[i] < final_upper[i - 1] or close.iloc[i - 1] > final_upper[i - 1]:
516            final_upper[i] = basic_upper.iloc[i]
517        else:
518            final_upper[i] = final_upper[i - 1]
519
520        # Final Lower Band
521        if basic_lower.iloc[i] > final_lower[i - 1] or close.iloc[i - 1] < final_lower[i - 1]:
522            final_lower[i] = basic_lower.iloc[i]
523        else:
524            final_lower[i] = final_lower[i - 1]
525
526        # Supertrend and Direction
527        if supertrend[i - 1] == final_upper[i - 1]:
528            # Was bearish
529            if close.iloc[i] > final_upper[i]:
530                supertrend[i] = final_lower[i]
531                direction[i] = 1  # Bullish
532            else:
533                supertrend[i] = final_upper[i]
534                direction[i] = -1  # Bearish
535        else:
536            # Was bullish
537            if close.iloc[i] < final_lower[i]:
538                supertrend[i] = final_upper[i]
539                direction[i] = -1  # Bearish
540            else:
541                supertrend[i] = final_lower[i]
542                direction[i] = 1  # Bullish
543
544    return pd.DataFrame(
545        {
546            "Supertrend": supertrend,
547            "Supertrend_Direction": direction,
548            "Supertrend_Upper": final_upper,
549            "Supertrend_Lower": final_lower,
550        },
551        index=df.index,
552    )

Calculate Supertrend indicator.

Supertrend is a trend-following indicator based on ATR.

  • When price is above Supertrend line: Bullish (uptrend)
  • When price is below Supertrend line: Bearish (downtrend)

Args: df: DataFrame with High, Low, Close columns atr_period: Period for ATR calculation (default: 10) multiplier: ATR multiplier for bands (default: 3.0)

Returns: DataFrame with columns: - Supertrend: The Supertrend line value - Supertrend_Direction: 1 for bullish, -1 for bearish - Supertrend_Upper: Upper band - Supertrend_Lower: Lower band

def calculate_tilson_t3( df: pandas.core.frame.DataFrame, period: int = 5, vfactor: float = 0.7, column: str = 'Close') -> pandas.core.series.Series:
 93def calculate_tilson_t3(
 94    df: pd.DataFrame,
 95    period: int = 5,
 96    vfactor: float = 0.7,
 97    column: str = "Close",
 98) -> pd.Series:
 99    """Calculate Tilson T3 Moving Average.
100
101    T3 is a triple-smoothed exponential moving average that reduces lag
102    while maintaining smoothness. Developed by Tim Tilson.
103
104    The T3 uses a volume factor (vfactor) to control the amount of
105    smoothing vs responsiveness:
106    - vfactor = 0: T3 behaves like a triple EMA
107    - vfactor = 1: Maximum smoothing (may overshoot)
108    - vfactor = 0.7: Tilson's recommended default
109
110    Args:
111        df: DataFrame with price data
112        period: Number of periods for EMA calculations (default 5)
113        vfactor: Volume factor for smoothing (0-1, default 0.7)
114        column: Column name to use for calculation
115
116    Returns:
117        Series with T3 values
118
119    Examples:
120        >>> t3 = calculate_tilson_t3(df, period=5, vfactor=0.7)
121        >>> # More responsive (less smooth)
122        >>> t3_fast = calculate_tilson_t3(df, period=5, vfactor=0.5)
123        >>> # More smooth (more lag)
124        >>> t3_smooth = calculate_tilson_t3(df, period=5, vfactor=0.9)
125    """
126    col = _get_price_column(df, column)
127    if col not in df.columns:
128        return pd.Series(np.nan, index=df.index, name=f"T3_{period}")
129
130    # Calculate coefficients
131    c1 = -(vfactor**3)
132    c2 = 3 * vfactor**2 + 3 * vfactor**3
133    c3 = -6 * vfactor**2 - 3 * vfactor - 3 * vfactor**3
134    c4 = 1 + 3 * vfactor + vfactor**3 + 3 * vfactor**2
135
136    # Calculate 6 consecutive EMAs
137    ema1 = df[col].ewm(span=period, adjust=False).mean()
138    ema2 = ema1.ewm(span=period, adjust=False).mean()
139    ema3 = ema2.ewm(span=period, adjust=False).mean()
140    ema4 = ema3.ewm(span=period, adjust=False).mean()
141    ema5 = ema4.ewm(span=period, adjust=False).mean()
142    ema6 = ema5.ewm(span=period, adjust=False).mean()
143
144    # T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3
145    t3 = c1 * ema6 + c2 * ema5 + c3 * ema4 + c4 * ema3
146
147    return t3.rename(f"T3_{period}")

Calculate Tilson T3 Moving Average.

T3 is a triple-smoothed exponential moving average that reduces lag while maintaining smoothness. Developed by Tim Tilson.

The T3 uses a volume factor (vfactor) to control the amount of smoothing vs responsiveness:

  • vfactor = 0: T3 behaves like a triple EMA
  • vfactor = 1: Maximum smoothing (may overshoot)
  • vfactor = 0.7: Tilson's recommended default

Args: df: DataFrame with price data period: Number of periods for EMA calculations (default 5) vfactor: Volume factor for smoothing (0-1, default 0.7) column: Column name to use for calculation

Returns: Series with T3 values

Examples:

t3 = calculate_tilson_t3(df, period=5, vfactor=0.7)

More responsive (less smooth)

t3_fast = calculate_tilson_t3(df, period=5, vfactor=0.5)

More smooth (more lag)

t3_smooth = calculate_tilson_t3(df, period=5, vfactor=0.9)

def calculate_heikin_ashi(df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame:
 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

def create_replay( symbol: str, period: str = '1y', interval: str = '1d', speed: float = 1.0, realtime_injection: bool = False) -> ReplaySession:
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'])

class BorsapyError(builtins.Exception):
5class BorsapyError(Exception):
6    """Base exception for all borsapy errors."""
7
8    pass

Base exception for all borsapy errors.

class TickerNotFoundError(borsapy.BorsapyError):
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.

TickerNotFoundError(symbol: str)
14    def __init__(self, symbol: str):
15        self.symbol = symbol
16        super().__init__(f"Ticker not found: {symbol}")
symbol
class DataNotAvailableError(borsapy.BorsapyError):
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.

DataNotAvailableError(message: str = 'Data not available')
22    def __init__(self, message: str = "Data not available"):
23        super().__init__(message)
class APIError(borsapy.BorsapyError):
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.

APIError(message: str, status_code: int | None = None)
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        )
status_code
class AuthenticationError(borsapy.BorsapyError):
36class AuthenticationError(BorsapyError):
37    """Raised when authentication fails."""
38
39    pass

Raised when authentication fails.

class RateLimitError(borsapy.BorsapyError):
42class RateLimitError(BorsapyError):
43    """Raised when rate limit is exceeded."""
44
45    pass

Raised when rate limit is exceeded.

class InvalidPeriodError(borsapy.BorsapyError):
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.

InvalidPeriodError(period: str)
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)}")
period
class InvalidIntervalError(borsapy.BorsapyError):
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.

InvalidIntervalError(interval: str)
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        )
interval
def set_tradingview_auth( username: str | None = None, password: str | None = None, session: str | None = None, session_sign: str | None = None) -> dict:
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:

  1. Username/password: Will perform login and get session tokens
  2. 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)

def get_tradingview_auth() -> dict | None:
88def get_tradingview_auth() -> dict | None:
89    """Get current TradingView authentication credentials."""
90    return _auth_credentials

Get current TradingView authentication credentials.

def clear_tradingview_auth() -> None:
82def clear_tradingview_auth() -> None:
83    """Clear TradingView authentication credentials."""
84    global _auth_credentials
85    _auth_credentials = None

Clear TradingView authentication credentials.

def create_stream(auth_token: str | None = None) -> TradingViewStream:
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()

class Backtest:
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)
Backtest( symbol: str, strategy: Callable[[dict, Optional[Literal['long', 'short']], dict], Optional[Literal['BUY', 'SELL', 'HOLD']]], period: str = '1y', interval: str = '1d', capital: float = 100000.0, commission: float = 0.001, indicators: list[str] | None = None, slippage: float = 0.0)
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).

WARMUP_PERIOD = 50
symbol
strategy
period
interval
capital
commission
indicators
slippage
def run(self) -> BacktestResult:
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.

@dataclass
class BacktestResult:
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.

BacktestResult( symbol: str, period: str, interval: str, strategy_name: str, initial_capital: float, commission: float, trades: list[Trade] = <factory>, equity_curve: pandas.core.series.Series = <factory>, drawdown_curve: pandas.core.series.Series = <factory>, buy_hold_curve: pandas.core.series.Series = <factory>)
symbol: str
period: str
interval: str
strategy_name: str
initial_capital: float
commission: float
trades: list[Trade]
equity_curve: pandas.core.series.Series
drawdown_curve: pandas.core.series.Series
buy_hold_curve: pandas.core.series.Series
final_equity: float
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.

net_profit: float
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.

net_profit_pct: float
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.

total_trades: int
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.

winning_trades: int
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.

losing_trades: int
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.

win_rate: float
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.

profit_factor: float
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.

avg_trade: float
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.

avg_winning_trade: float
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.

avg_losing_trade: float
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.

max_consecutive_wins: int
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.

max_consecutive_losses: int
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.

sharpe_ratio: float
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.

sortino_ratio: float
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.

max_drawdown: float
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.

max_drawdown_duration: int
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.

buy_hold_return: float
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.

vs_buy_hold: float
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).

calmar_ratio: float
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).

trades_df: pandas.core.frame.DataFrame
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.

def to_dict(self) -> dict[str, typing.Any]:
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.

def summary(self) -> str:
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.

@dataclass
class Trade:
 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).

Trade( entry_time: datetime.datetime, entry_price: float, exit_time: datetime.datetime | None = None, exit_price: float | None = None, side: Literal['long', 'short'] = 'long', shares: float = 0.0, commission: float = 0.0)
entry_time: datetime.datetime
entry_price: float
exit_time: datetime.datetime | None = None
exit_price: float | None = None
side: Literal['long', 'short'] = 'long'
shares: float = 0.0
commission: float = 0.0
is_closed: bool
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.

profit: float | None
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).

profit_pct: float | None
 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).

duration: float | None
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).

def to_dict(self) -> dict[str, typing.Any]:
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.

def backtest( symbol: str, strategy: Callable[[dict, Optional[Literal['long', 'short']], dict], Optional[Literal['BUY', 'SELL', 'HOLD']]], period: str = '1y', interval: str = '1d', capital: float = 100000.0, commission: float = 0.001, indicators: list[str] | None = None) -> BacktestResult:
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}")