# 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'])

View Source

      1"""
      2borsapy - Turkish Financial Markets Data Library
      3
      4A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data.
      5
      6Examples:
      7    >>> import borsapy as bp
      8
      9    # Get stock data
     10    >>> stock = bp.Ticker("THYAO")
     11    >>> stock.info  # Real-time quote
     12    >>> stock.history(period="1mo")  # OHLCV data
     13    >>> stock.balance_sheet  # Financial statements
     14
     15    # Get forex/commodity data
     16    >>> usd = bp.FX("USD")
     17    >>> usd.current  # Current rate
     18    >>> usd.history(period="1mo")  # Historical data
     19    >>> usd.bank_rates  # Bank exchange rates
     20    >>> usd.bank_rate("akbank")  # Single bank rate
     21    >>> bp.banks()  # List supported banks
     22    >>> gold = bp.FX("gram-altin")
     23
     24    # List all BIST companies
     25    >>> bp.companies()
     26    >>> bp.search_companies("banka")
     27
     28    # Get crypto data
     29    >>> btc = bp.Crypto("BTCTRY")
     30    >>> btc.current  # Current price
     31    >>> btc.history(period="1mo")  # Historical OHLCV
     32    >>> bp.crypto_pairs()  # List available pairs
     33
     34    # Get fund data
     35    >>> fund = bp.Fund("AAK")
     36    >>> fund.info  # Fund details
     37    >>> fund.history(period="1mo")  # Price history
     38
     39    # Get inflation data
     40    >>> inf = bp.Inflation()
     41    >>> inf.latest()  # Latest TÜFE data
     42    >>> inf.calculate(100000, "2020-01", "2024-01")  # Inflation calculation
     43
     44    # Economic calendar
     45    >>> cal = bp.EconomicCalendar()
     46    >>> cal.events(period="1w")  # This week's events
     47    >>> cal.today()  # Today's events
     48    >>> bp.economic_calendar(country="TR", importance="high")
     49
     50    # Government bonds
     51    >>> bp.bonds()  # All bond yields
     52    >>> bond = bp.Bond("10Y")
     53    >>> bond.yield_rate  # Current 10Y yield
     54    >>> bp.risk_free_rate()  # For DCF calculations
     55
     56    # Stock screener
     57    >>> bp.screen_stocks(template="high_dividend")
     58    >>> bp.screen_stocks(market_cap_min=1000, pe_max=15)
     59    >>> screener = bp.Screener()
     60    >>> screener.add_filter("dividend_yield", min=3).run()
     61
     62    # Real-time streaming (low-latency)
     63    >>> stream = bp.TradingViewStream()
     64    >>> stream.connect()
     65    >>> stream.subscribe("THYAO")
     66    >>> quote = stream.get_quote("THYAO")  # Instant (~1ms)
     67    >>> quote['last']  # Last price
     68    >>> stream.disconnect()
     69
     70    # Context manager
     71    >>> with bp.TradingViewStream() as stream:
     72    ...     stream.subscribe("THYAO")
     73    ...     quote = stream.wait_for_quote("THYAO")
     74    ...     print(quote['last'])
     75
     76    # Symbol search
     77    >>> bp.search("banka")  # Search all markets
     78    >>> bp.search_bist("enerji")  # BIST only
     79    >>> bp.search_crypto("BTC")  # Crypto only
     80    >>> bp.search("THYAO", full_info=True)  # Detailed results
     81
     82    # Heikin Ashi charts
     83    >>> df = stock.history(period="1y")
     84    >>> ha_df = bp.calculate_heikin_ashi(df)  # Returns HA_Open, HA_High, HA_Low, HA_Close
     85    >>> ha_df = stock.heikin_ashi(period="1y")  # Convenience method
     86
     87    # Chart streaming (OHLCV candles via WebSocket)
     88    >>> stream = bp.TradingViewStream()
     89    >>> stream.connect()
     90    >>> stream.subscribe_chart("THYAO", "1m")  # 1-minute candles
     91    >>> candle = stream.get_candle("THYAO", "1m")
     92    >>> print(candle['close'])
     93
     94    # Historical replay for backtesting
     95    >>> session = bp.create_replay("THYAO", period="1y", speed=100)
     96    >>> for candle in session.replay():
     97    ...     print(f"{candle['timestamp']}: {candle['close']}")
     98
     99    # Backtest engine
    100    >>> def rsi_strategy(candle, position, indicators):
    101    ...     if indicators['rsi'] < 30 and position is None:
    102    ...         return 'BUY'
    103    ...     elif indicators['rsi'] > 70 and position == 'long':
    104    ...         return 'SELL'
    105    ...     return 'HOLD'
    106    >>> result = bp.backtest("THYAO", rsi_strategy, period="1y", indicators=['rsi'])
    107    >>> print(result.summary())
    108    >>> print(f"Win Rate: {result.win_rate:.1f}%")
    109
    110    # Pine Script streaming indicators
    111    >>> stream = bp.TradingViewStream()
    112    >>> stream.connect()
    113    >>> stream.subscribe_chart("THYAO", "1m")
    114    >>> stream.add_study("THYAO", "1m", "RSI")
    115    >>> stream.add_study("THYAO", "1m", "MACD")
    116    >>> rsi = stream.get_study("THYAO", "1m", "RSI")
    117    >>> print(rsi['value'])
    118"""
    119
    120# TradingView authentication for real-time data
    121from borsapy._providers.tradingview import (
    122    clear_tradingview_auth,
    123    get_tradingview_auth,
    124    set_tradingview_auth,
    125)
    126
    127# Twitter/X authentication (optional, requires borsapy[twitter])
    128from borsapy._providers.twitter import (
    129    clear_twitter_auth,
    130    get_twitter_auth,
    131    set_twitter_auth,
    132)
    133from borsapy.backtest import Backtest, BacktestResult, Trade, backtest
    134from borsapy.bond import Bond, bonds, risk_free_rate
    135from borsapy.calendar import EconomicCalendar, economic_calendar
    136from borsapy.charts import calculate_heikin_ashi
    137from borsapy.crypto import Crypto, crypto_pairs
    138from borsapy.eurobond import Eurobond, eurobonds
    139from borsapy.exceptions import (
    140    APIError,
    141    AuthenticationError,
    142    BorsapyError,
    143    DataNotAvailableError,
    144    InvalidIntervalError,
    145    InvalidPeriodError,
    146    RateLimitError,
    147    TickerNotFoundError,
    148)
    149from borsapy.fund import Fund, compare_funds, management_fees, screen_funds, search_funds
    150from borsapy.fx import FX, banks, metal_institutions
    151from borsapy.index import Index, all_indices, index, indices
    152from borsapy.inflation import Inflation
    153from borsapy.market import companies, search_companies
    154from borsapy.multi import Tickers, download
    155from borsapy.portfolio import Portfolio
    156from borsapy.replay import ReplaySession, create_replay
    157from borsapy.scanner import ScanResult, TechnicalScanner, scan
    158from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices
    159from borsapy.search import (
    160    search,
    161    search_bist,
    162    search_crypto,
    163    search_forex,
    164    search_index,
    165    search_viop,
    166    viop_contracts,
    167)
    168
    169# TradingView streaming for real-time updates
    170from borsapy.stream import TradingViewStream, create_stream
    171from borsapy.tax import withholding_tax_rate, withholding_tax_table
    172from borsapy.tcmb import TCMB, policy_rate
    173from borsapy.technical import (
    174    TechnicalAnalyzer,
    175    add_indicators,
    176    calculate_adx,
    177    calculate_atr,
    178    calculate_bollinger_bands,
    179    calculate_dema,
    180    calculate_ema,
    181    calculate_hhv,
    182    calculate_llv,
    183    calculate_macd,
    184    calculate_mom,
    185    calculate_obv,
    186    calculate_roc,
    187    calculate_rsi,
    188    calculate_sma,
    189    calculate_stochastic,
    190    calculate_supertrend,
    191    calculate_tema,
    192    calculate_tilson_t3,
    193    calculate_vwap,
    194    calculate_wma,
    195)
    196from borsapy.ticker import Ticker
    197from borsapy.twitter import search_tweets
    198from borsapy.viop import VIOP
    199
    200__version__ = "0.8.4"
    201__author__ = "Said Surucu"
    202
    203__all__ = [
    204    # Main classes
    205    "Ticker",
    206    "Tickers",
    207    "FX",
    208    "Crypto",
    209    "Fund",
    210    "Portfolio",
    211    "Index",
    212    "Inflation",
    213    "VIOP",
    214    "Bond",
    215    "Eurobond",
    216    "TCMB",
    217    "EconomicCalendar",
    218    "Screener",
    219    "TradingViewStream",
    220    "ReplaySession",
    221    # Market functions
    222    "companies",
    223    "search_companies",
    224    "search",
    225    "search_bist",
    226    "search_crypto",
    227    "search_forex",
    228    "search_index",
    229    "search_viop",
    230    "viop_contracts",
    231    "banks",
    232    "metal_institutions",
    233    "crypto_pairs",
    234    "search_funds",
    235    "screen_funds",
    236    "compare_funds",
    237    "management_fees",
    238    "download",
    239    "index",
    240    "indices",
    241    "all_indices",
    242    # Bond functions
    243    "bonds",
    244    "risk_free_rate",
    245    # Eurobond functions
    246    "eurobonds",
    247    # TCMB functions
    248    "policy_rate",
    249    # Calendar functions
    250    "economic_calendar",
    251    # Screener functions
    252    "screen_stocks",
    253    "screener_criteria",
    254    "sectors",
    255    "stock_indices",
    256    # Technical Scanner
    257    "TechnicalScanner",
    258    "ScanResult",
    259    "scan",
    260    # Technical analysis
    261    "TechnicalAnalyzer",
    262    "add_indicators",
    263    "calculate_sma",
    264    "calculate_ema",
    265    "calculate_rsi",
    266    "calculate_macd",
    267    "calculate_bollinger_bands",
    268    "calculate_atr",
    269    "calculate_stochastic",
    270    "calculate_obv",
    271    "calculate_vwap",
    272    "calculate_adx",
    273    "calculate_supertrend",
    274    "calculate_tilson_t3",
    275    # MetaStock indicators
    276    "calculate_hhv",
    277    "calculate_llv",
    278    "calculate_mom",
    279    "calculate_roc",
    280    "calculate_wma",
    281    "calculate_dema",
    282    "calculate_tema",
    283    # Charts
    284    "calculate_heikin_ashi",
    285    # Replay
    286    "ReplaySession",
    287    "create_replay",
    288    # Exceptions
    289    "BorsapyError",
    290    "TickerNotFoundError",
    291    "DataNotAvailableError",
    292    "APIError",
    293    "AuthenticationError",
    294    "RateLimitError",
    295    "InvalidPeriodError",
    296    "InvalidIntervalError",
    297    # TradingView authentication (premium)
    298    "set_tradingview_auth",
    299    "get_tradingview_auth",
    300    "clear_tradingview_auth",
    301    # TradingView streaming (real-time)
    302    "TradingViewStream",
    303    "create_stream",
    304    # Backtest engine
    305    "Backtest",
    306    "BacktestResult",
    307    "Trade",
    308    "backtest",
    309    # Tax
    310    "withholding_tax_rate",
    311    "withholding_tax_table",
    312    # Twitter/X (optional, requires borsapy[twitter])
    313    "set_twitter_auth",
    314    "get_twitter_auth",
    315    "clear_twitter_auth",
    316    "search_tweets",
    317]

class Ticker(borsapy.technical.TechnicalMixin, borsapy.twitter.TwitterMixin): View Source

     469class Ticker(TechnicalMixin, TwitterMixin):
     470    """
     471    A yfinance-like interface for Turkish stock data.
     472
     473    Examples:
     474        >>> import borsapy as bp
     475        >>> stock = bp.Ticker("THYAO")
     476        >>> stock.info
     477        {'symbol': 'THYAO', 'last': 268.5, ...}
     478        >>> stock.history(period="1mo")
     479                         Open    High     Low   Close    Volume
     480        Date
     481        2024-12-01    265.00  268.00  264.00  267.50  12345678
     482        ...
     483    """
     484
     485    def __init__(self, symbol: str):
     486        """
     487        Initialize a Ticker object.
     488
     489        Args:
     490            symbol: Stock symbol (e.g., "THYAO", "GARAN", "ASELS").
     491                    The ".IS" or ".E" suffix is optional and will be removed.
     492        """
     493        self._symbol = symbol.upper().replace(".IS", "").replace(".E", "")
     494        self._tradingview = get_tradingview_provider()
     495        self._isyatirim = None  # Lazy load for financial statements
     496        self._kap = None  # Lazy load for KAP disclosures
     497        self._isin_provider = None  # Lazy load for ISIN lookup
     498        self._hedeffiyat = None  # Lazy load for analyst price targets
     499        self._etf_provider = None  # Lazy load for ETF holders
     500
     501    def _get_tweet_query(self) -> str:
     502        return _build_stock_query(self._symbol)
     503
     504    def _get_isyatirim(self):
     505        """Lazy load İş Yatırım provider for financial statements."""
     506        if self._isyatirim is None:
     507            from borsapy._providers.isyatirim import get_isyatirim_provider
     508
     509            self._isyatirim = get_isyatirim_provider()
     510        return self._isyatirim
     511
     512    def _get_kap(self):
     513        """Lazy load KAP provider for disclosures and calendar."""
     514        if self._kap is None:
     515            from borsapy._providers.kap import get_kap_provider
     516
     517            self._kap = get_kap_provider()
     518        return self._kap
     519
     520    def _get_isin_provider(self):
     521        """Lazy load ISIN provider."""
     522        if self._isin_provider is None:
     523            from borsapy._providers.isin import get_isin_provider
     524
     525            self._isin_provider = get_isin_provider()
     526        return self._isin_provider
     527
     528    def _get_hedeffiyat(self):
     529        """Lazy load hedeffiyat.com.tr provider for analyst price targets."""
     530        if self._hedeffiyat is None:
     531            from borsapy._providers.hedeffiyat import get_hedeffiyat_provider
     532
     533            self._hedeffiyat = get_hedeffiyat_provider()
     534        return self._hedeffiyat
     535
     536    def _get_etf_provider(self):
     537        """Lazy load TradingView ETF provider for ETF holders."""
     538        if self._etf_provider is None:
     539            from borsapy._providers.tradingview_etf import get_tradingview_etf_provider
     540
     541            self._etf_provider = get_tradingview_etf_provider()
     542        return self._etf_provider
     543
     544    @property
     545    def symbol(self) -> str:
     546        """Return the ticker symbol."""
     547        return self._symbol
     548
     549    @property
     550    def fast_info(self) -> FastInfo:
     551        """
     552        Get fast access to common ticker information.
     553
     554        Returns a FastInfo object with quick access to frequently used data:
     555        - currency, exchange, timezone
     556        - last_price, open, day_high, day_low, previous_close, volume
     557        - market_cap, shares, pe_ratio, pb_ratio
     558        - year_high, year_low (52-week)
     559        - fifty_day_average, two_hundred_day_average
     560        - free_float, foreign_ratio
     561
     562        Examples:
     563            >>> stock = Ticker("THYAO")
     564            >>> stock.fast_info.market_cap
     565            370530000000
     566            >>> stock.fast_info['pe_ratio']
     567            2.8
     568            >>> stock.fast_info.keys()
     569            ['currency', 'exchange', 'timezone', ...]
     570        """
     571        if not hasattr(self, "_fast_info"):
     572            self._fast_info = FastInfo(self)
     573        return self._fast_info
     574
     575    @property
     576    def info(self) -> EnrichedInfo:
     577        """
     578        Get comprehensive ticker information with yfinance-compatible fields.
     579
     580        Returns:
     581            EnrichedInfo object providing dict-like access to:
     582
     583            Basic fields (always loaded, fast):
     584            - symbol, last, open, high, low, close, volume
     585            - change, change_percent, update_time
     586
     587            yfinance aliases (map to basic fields):
     588            - regularMarketPrice, currentPrice -> last
     589            - regularMarketOpen -> open
     590            - regularMarketDayHigh -> high
     591            - regularMarketDayLow -> low
     592            - regularMarketPreviousClose -> close
     593            - regularMarketVolume -> volume
     594
     595            Extended fields (lazy-loaded on access):
     596            - marketCap, trailingPE, priceToBook, enterpriseToEbitda
     597            - sharesOutstanding, fiftyTwoWeekHigh, fiftyTwoWeekLow
     598            - fiftyDayAverage, twoHundredDayAverage
     599            - floatShares, foreignRatio, netDebt
     600            - currency, exchange, timezone
     601
     602            Dividend fields (lazy-loaded on access):
     603            - dividendYield, exDividendDate
     604            - trailingAnnualDividendRate, trailingAnnualDividendYield
     605
     606        Examples:
     607            >>> stock = Ticker("THYAO")
     608            >>> stock.info['last']  # Basic field - fast
     609            268.5
     610            >>> stock.info['marketCap']  # Extended field - fetches İş Yatırım
     611            370530000000
     612            >>> stock.info['trailingPE']  # yfinance compatible name
     613            2.8
     614            >>> stock.info.get('dividendYield')  # Safe access
     615            1.28
     616            >>> stock.info.todict()  # Get all as regular dict
     617            {...}
     618        """
     619        if not hasattr(self, "_enriched_info"):
     620            self._enriched_info = EnrichedInfo(self)
     621        return self._enriched_info
     622
     623    def history(
     624        self,
     625        period: str = "1mo",
     626        interval: str = "1d",
     627        start: datetime | str | None = None,
     628        end: datetime | str | None = None,
     629        actions: bool = False,
     630        adjust: bool = True,
     631    ) -> pd.DataFrame:
     632        """
     633        Get historical OHLCV data.
     634
     635        Args:
     636            period: How much data to fetch. Valid periods:
     637                    1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max.
     638                    Ignored if start is provided.
     639            interval: Data granularity. Valid intervals:
     640                      1m, 5m, 15m, 30m, 1h, 1d, 1wk, 1mo.
     641            start: Start date (string or datetime).
     642            end: End date (string or datetime). Defaults to today.
     643            actions: If True, include Dividends and Stock Splits columns.
     644                     Defaults to False.
     645            adjust: If True (default), return split-adjusted prices.
     646                    If False, return unadjusted (raw) prices.
     647
     648        Returns:
     649            DataFrame with columns: Open, High, Low, Close, Volume.
     650            If actions=True, also includes Dividends and Stock Splits columns.
     651            Index is the Date.
     652
     653        Examples:
     654            >>> stock = Ticker("THYAO")
     655            >>> stock.history(period="1mo")  # Last month
     656            >>> stock.history(period="1y", interval="1wk")  # Weekly for 1 year
     657            >>> stock.history(start="2024-01-01", end="2024-06-30")  # Date range
     658            >>> stock.history(period="1y", actions=True)  # With dividends/splits
     659            >>> stock.history(period="max", adjust=False)  # Raw unadjusted prices
     660        """
     661        # Parse dates if strings
     662        start_dt = self._parse_date(start) if start else None
     663        end_dt = self._parse_date(end) if end else None
     664
     665        df = self._tradingview.get_history(
     666            symbol=self._symbol,
     667            period=period,
     668            interval=interval,
     669            start=start_dt,
     670            end=end_dt,
     671        )
     672
     673        if not adjust and not df.empty:
     674            df = self._unadjust_prices(df)
     675
     676        if actions and not df.empty:
     677            df = self._add_actions_to_history(df)
     678
     679        return df
     680
     681    def _add_actions_to_history(self, df: pd.DataFrame) -> pd.DataFrame:
     682        """
     683        Add Dividends and Stock Splits columns to historical data.
     684
     685        Args:
     686            df: Historical OHLCV DataFrame.
     687
     688        Returns:
     689            DataFrame with added Dividends and Stock Splits columns.
     690        """
     691        # Initialize columns with zeros
     692        df = df.copy()
     693        df["Dividends"] = 0.0
     694        df["Stock Splits"] = 0.0
     695
     696        # Get dividends
     697        try:
     698            divs = self.dividends
     699            if not divs.empty:
     700                for div_date, row in divs.iterrows():
     701                    # Use date() for timezone-agnostic comparison
     702                    div_date_only = pd.Timestamp(div_date).date()
     703                    for idx in df.index:
     704                        idx_date_only = pd.Timestamp(idx).date()
     705                        if div_date_only == idx_date_only:
     706                            df.loc[idx, "Dividends"] = row.get("Amount", 0)
     707                            break
     708        except Exception:
     709            pass
     710
     711        # Get stock splits (capital increases)
     712        try:
     713            splits = self.splits
     714            if not splits.empty:
     715                for split_date, row in splits.iterrows():
     716                    # Use date() for timezone-agnostic comparison
     717                    split_date_only = pd.Timestamp(split_date).date()
     718                    # Calculate split ratio
     719                    # BonusFromCapital + BonusFromDividend = total bonus percentage
     720                    bonus_pct = row.get("BonusFromCapital", 0) + row.get(
     721                        "BonusFromDividend", 0
     722                    )
     723                    if bonus_pct > 0:
     724                        # Convert percentage to split ratio (e.g., 20% bonus = 1.2 split)
     725                        split_ratio = 1 + (bonus_pct / 100)
     726                        for idx in df.index:
     727                            idx_date_only = pd.Timestamp(idx).date()
     728                            if split_date_only == idx_date_only:
     729                                df.loc[idx, "Stock Splits"] = split_ratio
     730                                break
     731        except Exception:
     732            pass
     733
     734        return df
     735
     736    def _unadjust_prices(self, df: pd.DataFrame) -> pd.DataFrame:
     737        """
     738        Reverse split adjustments to return raw unadjusted prices.
     739
     740        TradingView returns split-adjusted prices by default. This method
     741        uses the splits data from İş Yatırım to reverse the adjustments.
     742        """
     743        try:
     744            splits = self.splits
     745        except Exception:
     746            return df
     747
     748        if splits.empty:
     749            return df
     750
     751        # Collect split events with bonus ratios
     752        split_events = []
     753        for split_date, row in splits.iterrows():
     754            bonus_pct = row.get("BonusFromCapital", 0) + row.get(
     755                "BonusFromDividend", 0
     756            )
     757            if bonus_pct > 0:
     758                ratio = 1 + (bonus_pct / 100)
     759                split_events.append(
     760                    (pd.Timestamp(split_date, tz="Europe/Istanbul"), ratio)
     761                )
     762
     763        if not split_events:
     764            return df
     765
     766        # Sort by date ascending
     767        split_events.sort(key=lambda x: x[0])
     768
     769        df = df.copy()
     770
     771        # Build a vectorized adjustment factor series.
     772        # For each split, all rows BEFORE the split date get multiplied by the ratio.
     773        factor = pd.Series(1.0, index=df.index)
     774        for split_date, ratio in split_events:
     775            factor = factor * pd.Series(
     776                [ratio if idx < split_date else 1.0 for idx in df.index],
     777                index=df.index,
     778            )
     779
     780        price_cols = [c for c in ["Open", "High", "Low", "Close"] if c in df.columns]
     781        for col in price_cols:
     782            df[col] = df[col] * factor
     783        if "Volume" in df.columns:
     784            df["Volume"] = df["Volume"] / factor
     785
     786        return df
     787
     788    @cached_property
     789    def dividends(self) -> pd.DataFrame:
     790        """
     791        Get dividend history.
     792
     793        Returns:
     794            DataFrame with dividend history:
     795            - Amount: Dividend per share (TL)
     796            - GrossRate: Gross dividend rate (%)
     797            - NetRate: Net dividend rate (%)
     798            - TotalDividend: Total dividend distributed (TL)
     799
     800        Examples:
     801            >>> stock = Ticker("THYAO")
     802            >>> stock.dividends
     803                           Amount  GrossRate  NetRate  TotalDividend
     804            Date
     805            2025-09-02     3.442    344.20   292.57  4750000000.0
     806            2025-06-16     3.442    344.20   292.57  4750000000.0
     807        """
     808        return self._get_isyatirim().get_dividends(self._symbol)
     809
     810    @cached_property
     811    def splits(self) -> pd.DataFrame:
     812        """
     813        Get capital increase (split) history.
     814
     815        Note: Turkish market uses capital increases instead of traditional splits.
     816        - RightsIssue: Paid capital increase (bedelli)
     817        - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
     818        - BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
     819
     820        Returns:
     821            DataFrame with capital increase history:
     822            - Capital: New capital after increase (TL)
     823            - RightsIssue: Rights issue rate (%)
     824            - BonusFromCapital: Bonus from capital (%)
     825            - BonusFromDividend: Bonus from dividend (%)
     826
     827        Examples:
     828            >>> stock = Ticker("THYAO")
     829            >>> stock.splits
     830                             Capital  RightsIssue  BonusFromCapital  BonusFromDividend
     831            Date
     832            2013-06-26  1380000000.0         0.0             15.00               0.0
     833            2011-07-11  1200000000.0         0.0              0.00              20.0
     834        """
     835        return self._get_isyatirim().get_capital_increases(self._symbol)
     836
     837    @cached_property
     838    def actions(self) -> pd.DataFrame:
     839        """
     840        Get combined dividends and splits history.
     841
     842        Returns:
     843            DataFrame with combined dividend and split actions:
     844            - Dividends: Dividend per share (TL) or 0
     845            - Splits: Combined split ratio (0 if no split)
     846
     847        Examples:
     848            >>> stock = Ticker("THYAO")
     849            >>> stock.actions
     850                         Dividends  Splits
     851            Date
     852            2025-09-02      3.442    0.0
     853            2013-06-26      0.000   15.0
     854        """
     855        dividends = self.dividends
     856        splits = self.splits
     857
     858        # Merge on index (Date)
     859        if dividends.empty and splits.empty:
     860            return pd.DataFrame(columns=["Dividends", "Splits"])
     861
     862        # Extract relevant columns
     863        div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float)
     864        split_series = (
     865            splits["BonusFromCapital"] + splits["BonusFromDividend"]
     866            if not splits.empty
     867            else pd.Series(dtype=float)
     868        )
     869
     870        # Combine into single DataFrame
     871        result = pd.DataFrame({"Dividends": div_series, "Splits": split_series})
     872        result = result.fillna(0)
     873        result = result.sort_index(ascending=False)
     874
     875        return result
     876
     877    def get_balance_sheet(
     878        self,
     879        quarterly: bool = False,
     880        financial_group: str | None = None,
     881        last_n: int | str | None = None,
     882    ) -> pd.DataFrame:
     883        """
     884        Get balance sheet data.
     885
     886        Args:
     887            quarterly: If True, return quarterly data. If False, return annual.
     888            financial_group: Financial group code. Use "UFRS" for banks,
     889                           "XI_29" for industrial companies. If None, defaults to XI_29.
     890            last_n: Number of periods to fetch. None for default (5), int for exact
     891                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
     892
     893        Returns:
     894            DataFrame with balance sheet items as rows and periods as columns.
     895
     896        Examples:
     897            >>> stock = bp.Ticker("THYAO")
     898            >>> stock.get_balance_sheet()  # Annual, industrial (5 periods)
     899            >>> stock.get_balance_sheet(quarterly=True, last_n=20)  # 20 quarters
     900
     901            >>> bank = bp.Ticker("AKBNK")
     902            >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all")
     903        """
     904        return self._get_isyatirim().get_financial_statements(
     905            symbol=self._symbol,
     906            statement_type="balance_sheet",
     907            quarterly=quarterly,
     908            financial_group=financial_group,
     909            last_n=last_n,
     910        )
     911
     912    def get_income_stmt(
     913        self,
     914        quarterly: bool = False,
     915        financial_group: str | None = None,
     916        last_n: int | str | None = None,
     917    ) -> pd.DataFrame:
     918        """
     919        Get income statement data.
     920
     921        Args:
     922            quarterly: If True, return quarterly data. If False, return annual.
     923            financial_group: Financial group code. Use "UFRS" for banks,
     924                           "XI_29" for industrial companies. If None, defaults to XI_29.
     925            last_n: Number of periods to fetch. None for default (5), int for exact
     926                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
     927
     928        Returns:
     929            DataFrame with income statement items as rows and periods as columns.
     930
     931        Examples:
     932            >>> stock = bp.Ticker("THYAO")
     933            >>> stock.get_income_stmt()  # Annual (5 periods)
     934            >>> stock.get_income_stmt(quarterly=True, last_n=20)  # 20 quarters
     935
     936            >>> bank = bp.Ticker("AKBNK")
     937            >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
     938        """
     939        return self._get_isyatirim().get_financial_statements(
     940            symbol=self._symbol,
     941            statement_type="income_stmt",
     942            quarterly=quarterly,
     943            financial_group=financial_group,
     944            last_n=last_n,
     945        )
     946
     947    def get_cashflow(
     948        self,
     949        quarterly: bool = False,
     950        financial_group: str | None = None,
     951        last_n: int | str | None = None,
     952    ) -> pd.DataFrame:
     953        """
     954        Get cash flow statement data.
     955
     956        Args:
     957            quarterly: If True, return quarterly data. If False, return annual.
     958            financial_group: Financial group code. Use "UFRS" for banks,
     959                           "XI_29" for industrial companies. If None, defaults to XI_29.
     960            last_n: Number of periods to fetch. None for default (5), int for exact
     961                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
     962
     963        Returns:
     964            DataFrame with cash flow items as rows and periods as columns.
     965
     966        Examples:
     967            >>> stock = bp.Ticker("THYAO")
     968            >>> stock.get_cashflow()  # Annual (5 periods)
     969            >>> stock.get_cashflow(quarterly=True, last_n=20)  # 20 quarters
     970
     971            >>> bank = bp.Ticker("AKBNK")
     972            >>> bank.get_cashflow(financial_group="UFRS", last_n="all")
     973        """
     974        return self._get_isyatirim().get_financial_statements(
     975            symbol=self._symbol,
     976            statement_type="cashflow",
     977            quarterly=quarterly,
     978            financial_group=financial_group,
     979            last_n=last_n,
     980        )
     981
     982    # Legacy property aliases for backward compatibility
     983    @cached_property
     984    def balance_sheet(self) -> pd.DataFrame:
     985        """Annual balance sheet (use get_balance_sheet() for more options)."""
     986        return self.get_balance_sheet(quarterly=False)
     987
     988    @cached_property
     989    def quarterly_balance_sheet(self) -> pd.DataFrame:
     990        """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options)."""
     991        return self.get_balance_sheet(quarterly=True)
     992
     993    @cached_property
     994    def income_stmt(self) -> pd.DataFrame:
     995        """Annual income statement (use get_income_stmt() for more options)."""
     996        return self.get_income_stmt(quarterly=False)
     997
     998    @cached_property
     999    def quarterly_income_stmt(self) -> pd.DataFrame:
    1000        """Quarterly income statement (use get_income_stmt(quarterly=True) for more options)."""
    1001        return self.get_income_stmt(quarterly=True)
    1002
    1003    @cached_property
    1004    def cashflow(self) -> pd.DataFrame:
    1005        """Annual cash flow (use get_cashflow() for more options)."""
    1006        return self.get_cashflow(quarterly=False)
    1007
    1008    @cached_property
    1009    def quarterly_cashflow(self) -> pd.DataFrame:
    1010        """Quarterly cash flow (use get_cashflow(quarterly=True) for more options)."""
    1011        return self.get_cashflow(quarterly=True)
    1012
    1013    def _calculate_ttm(self, quarterly_df: pd.DataFrame) -> pd.DataFrame:
    1014        """
    1015        Calculate trailing twelve months (TTM) by summing last 4 quarters.
    1016
    1017        Args:
    1018            quarterly_df: DataFrame with quarterly data (columns in YYYYQN format).
    1019
    1020        Returns:
    1021            DataFrame with single TTM column containing summed values.
    1022        """
    1023        if quarterly_df.empty or len(quarterly_df.columns) < 4:
    1024            return pd.DataFrame(columns=["TTM"])
    1025
    1026        # First 4 columns = last 4 quarters (most recent first)
    1027        last_4_quarters = quarterly_df.iloc[:, :4]
    1028
    1029        # Convert to numeric, coercing errors to NaN
    1030        numeric_df = last_4_quarters.apply(pd.to_numeric, errors="coerce")
    1031
    1032        return numeric_df.sum(axis=1).to_frame(name="TTM")
    1033
    1034    def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame:
    1035        """
    1036        Get trailing twelve months (TTM) income statement.
    1037
    1038        Calculates TTM by summing the last 4 quarters of income statement data.
    1039
    1040        Args:
    1041            financial_group: Financial group code. Use "UFRS" for banks,
    1042                           "XI_29" for industrial companies. If None, defaults to XI_29.
    1043
    1044        Returns:
    1045            DataFrame with TTM column containing summed values for each line item.
    1046
    1047        Examples:
    1048            >>> stock = bp.Ticker("THYAO")
    1049            >>> stock.get_ttm_income_stmt()
    1050
    1051            >>> bank = bp.Ticker("AKBNK")
    1052            >>> bank.get_ttm_income_stmt(financial_group="UFRS")
    1053        """
    1054        quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group)
    1055        return self._calculate_ttm(quarterly)
    1056
    1057    def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame:
    1058        """
    1059        Get trailing twelve months (TTM) cash flow statement.
    1060
    1061        Calculates TTM by summing the last 4 quarters of cash flow data.
    1062
    1063        Args:
    1064            financial_group: Financial group code. Use "UFRS" for banks,
    1065                           "XI_29" for industrial companies. If None, defaults to XI_29.
    1066
    1067        Returns:
    1068            DataFrame with TTM column containing summed values for each line item.
    1069
    1070        Examples:
    1071            >>> stock = bp.Ticker("THYAO")
    1072            >>> stock.get_ttm_cashflow()
    1073
    1074            >>> bank = bp.Ticker("AKBNK")
    1075            >>> bank.get_ttm_cashflow(financial_group="UFRS")
    1076        """
    1077        quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group)
    1078        return self._calculate_ttm(quarterly)
    1079
    1080    # Legacy property aliases
    1081    @cached_property
    1082    def ttm_income_stmt(self) -> pd.DataFrame:
    1083        """TTM income statement (use get_ttm_income_stmt() for banks)."""
    1084        return self.get_ttm_income_stmt()
    1085
    1086    @cached_property
    1087    def ttm_cashflow(self) -> pd.DataFrame:
    1088        """TTM cash flow (use get_ttm_cashflow() for banks)."""
    1089        return self.get_ttm_cashflow()
    1090
    1091    @cached_property
    1092    def major_holders(self) -> pd.DataFrame:
    1093        """
    1094        Get major shareholders (ortaklık yapısı).
    1095
    1096        Returns:
    1097            DataFrame with shareholder names and percentages:
    1098            - Index: Holder name
    1099            - Percentage: Ownership percentage (%)
    1100
    1101        Examples:
    1102            >>> stock = Ticker("THYAO")
    1103            >>> stock.major_holders
    1104                                     Percentage
    1105            Holder
    1106            Diğer                        50.88
    1107            Türkiye Varlık Fonu          49.12
    1108        """
    1109        return self._get_isyatirim().get_major_holders(self._symbol)
    1110
    1111    @cached_property
    1112    def recommendations(self) -> dict:
    1113        """
    1114        Get analyst recommendations and target price.
    1115
    1116        Returns:
    1117            Dictionary with:
    1118            - recommendation: Buy/Hold/Sell (AL/TUT/SAT)
    1119            - target_price: Analyst target price (TL)
    1120            - upside_potential: Expected upside (%)
    1121
    1122        Examples:
    1123            >>> stock = Ticker("THYAO")
    1124            >>> stock.recommendations
    1125            {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
    1126        """
    1127        return self._get_isyatirim().get_recommendations(self._symbol)
    1128
    1129    @cached_property
    1130    def recommendations_summary(self) -> dict[str, int]:
    1131        """
    1132        Get analyst recommendation summary with buy/hold/sell counts.
    1133
    1134        Aggregates individual analyst recommendations from hedeffiyat.com.tr
    1135        into yfinance-compatible categories.
    1136
    1137        Returns:
    1138            Dictionary with counts:
    1139            - strongBuy: Strong buy recommendations
    1140            - buy: Buy recommendations (includes "Endeks Üstü Getiri")
    1141            - hold: Hold recommendations (includes "Nötr", "Endekse Paralel")
    1142            - sell: Sell recommendations (includes "Endeks Altı Getiri")
    1143            - strongSell: Strong sell recommendations
    1144
    1145        Examples:
    1146            >>> stock = Ticker("THYAO")
    1147            >>> stock.recommendations_summary
    1148            {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
    1149        """
    1150        return self._get_hedeffiyat().get_recommendations_summary(self._symbol)
    1151
    1152    @cached_property
    1153    def news(self) -> pd.DataFrame:
    1154        """
    1155        Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
    1156
    1157        Fetches directly from KAP - the official disclosure platform for
    1158        publicly traded companies in Turkey.
    1159
    1160        Returns:
    1161            DataFrame with columns:
    1162            - Date: Disclosure date and time
    1163            - Title: Disclosure headline
    1164            - URL: Link to full disclosure on KAP
    1165
    1166        Examples:
    1167            >>> stock = Ticker("THYAO")
    1168            >>> stock.news
    1169                              Date                                         Title                                         URL
    1170            0  29.12.2025 19:21:18  Haber ve Söylentilere İlişkin Açıklama  https://www.kap.org.tr/tr/Bildirim/1530826
    1171            1  29.12.2025 16:11:36  Payların Geri Alınmasına İlişkin Bildirim  https://www.kap.org.tr/tr/Bildirim/1530656
    1172        """
    1173        return self._get_kap().get_disclosures(self._symbol)
    1174
    1175    def get_news_content(self, disclosure_id: int | str) -> str | None:
    1176        """
    1177        Get full HTML content of a KAP disclosure by ID.
    1178
    1179        Args:
    1180            disclosure_id: KAP disclosure ID from news DataFrame URL.
    1181
    1182        Returns:
    1183            Raw HTML content or None if failed.
    1184
    1185        Examples:
    1186            >>> stock = Ticker("THYAO")
    1187            >>> html = stock.get_news_content(1530826)
    1188        """
    1189        return self._get_kap().get_disclosure_content(disclosure_id)
    1190
    1191    @cached_property
    1192    def calendar(self) -> pd.DataFrame:
    1193        """
    1194        Get expected disclosure calendar for the stock from KAP.
    1195
    1196        Returns upcoming expected disclosures like financial reports,
    1197        annual reports, sustainability reports, and corporate governance reports.
    1198
    1199        Returns:
    1200            DataFrame with columns:
    1201            - StartDate: Expected disclosure window start
    1202            - EndDate: Expected disclosure window end
    1203            - Subject: Type of disclosure (e.g., "Finansal Rapor")
    1204            - Period: Report period (e.g., "Yıllık", "3 Aylık")
    1205            - Year: Fiscal year
    1206
    1207        Examples:
    1208            >>> stock = Ticker("THYAO")
    1209            >>> stock.calendar
    1210                  StartDate       EndDate               Subject   Period  Year
    1211            0  01.01.2026  11.03.2026       Finansal Rapor   Yıllık  2025
    1212            1  01.01.2026  11.03.2026    Faaliyet Raporu  Yıllık  2025
    1213            2  01.04.2026  11.05.2026       Finansal Rapor  3 Aylık  2026
    1214        """
    1215        return self._get_kap().get_calendar(self._symbol)
    1216
    1217    @cached_property
    1218    def isin(self) -> str | None:
    1219        """
    1220        Get ISIN (International Securities Identification Number) code.
    1221
    1222        ISIN is a 12-character alphanumeric code that uniquely identifies
    1223        a security, standardized by ISO 6166.
    1224
    1225        Returns:
    1226            ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
    1227
    1228        Examples:
    1229            >>> stock = Ticker("THYAO")
    1230            >>> stock.isin
    1231            'TRATHYAO91M5'
    1232        """
    1233        return self._get_isin_provider().get_isin(self._symbol)
    1234
    1235    @cached_property
    1236    def analyst_price_targets(self) -> dict[str, float | int | None]:
    1237        """
    1238        Get analyst price target data from hedeffiyat.com.tr.
    1239
    1240        Returns aggregated price target information from multiple analysts.
    1241
    1242        Returns:
    1243            Dictionary with:
    1244            - current: Current stock price
    1245            - low: Lowest analyst target price
    1246            - high: Highest analyst target price
    1247            - mean: Average target price
    1248            - median: Median target price
    1249            - numberOfAnalysts: Number of analysts covering the stock
    1250
    1251        Examples:
    1252            >>> stock = Ticker("THYAO")
    1253            >>> stock.analyst_price_targets
    1254            {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49,
    1255             'median': 465.0, 'numberOfAnalysts': 19}
    1256        """
    1257        return self._get_hedeffiyat().get_price_targets(self._symbol)
    1258
    1259    @property
    1260    def etf_holders(self) -> pd.DataFrame:
    1261        """
    1262        Get international ETFs that hold this stock.
    1263
    1264        Returns data from TradingView showing which ETFs hold this stock,
    1265        including position value, weight, and ETF characteristics.
    1266
    1267        Returns:
    1268            DataFrame with ETF holder information:
    1269            - symbol: ETF ticker symbol
    1270            - exchange: Exchange (AMEX, NASDAQ, LSE, etc.)
    1271            - name: ETF full name
    1272            - market_cap_usd: Position value in USD
    1273            - holding_weight_pct: Weight percentage (0.09 = 0.09%)
    1274            - issuer: ETF issuer (BlackRock, Vanguard, etc.)
    1275            - management: Management style (Passive/Active)
    1276            - focus: Investment focus (Total Market, Emerging Markets, etc.)
    1277            - expense_ratio: Expense ratio (0.09 = 0.09%)
    1278            - aum_usd: Total assets under management (USD)
    1279            - price: Current ETF price
    1280            - change_pct: Change percentage
    1281
    1282        Examples:
    1283            >>> stock = Ticker("ASELS")
    1284            >>> holders = stock.etf_holders
    1285            >>> holders[['symbol', 'name', 'holding_weight_pct']].head()
    1286               symbol                                      name  holding_weight_pct
    1287            0    IEMG  iShares Core MSCI Emerging Markets ETF            0.090686
    1288            1     VWO     Vanguard FTSE Emerging Markets ETF            0.060000
    1289
    1290            >>> print(f"Total ETFs: {len(holders)}")
    1291            Total ETFs: 118
    1292        """
    1293        return self._get_etf_provider().get_etf_holders(self._symbol)
    1294
    1295    @cached_property
    1296    def earnings_dates(self) -> pd.DataFrame:
    1297        """
    1298        Get upcoming earnings announcement dates.
    1299
    1300        Derived from KAP calendar, showing expected financial report dates.
    1301        Compatible with yfinance earnings_dates format.
    1302
    1303        Returns:
    1304            DataFrame with index as Earnings Date and columns:
    1305            - EPS Estimate: Always None (not available for BIST)
    1306            - Reported EPS: Always None (not available for BIST)
    1307            - Surprise (%): Always None (not available for BIST)
    1308
    1309        Examples:
    1310            >>> stock = Ticker("THYAO")
    1311            >>> stock.earnings_dates
    1312                            EPS Estimate  Reported EPS  Surprise(%)
    1313            Earnings Date
    1314            2026-03-11           None          None         None
    1315            2026-05-11           None          None         None
    1316        """
    1317        cal = self.calendar
    1318        if cal.empty:
    1319            return pd.DataFrame(
    1320                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1321            )
    1322
    1323        # Filter for financial reports only
    1324        financial_reports = cal[
    1325            cal["Subject"].str.contains("Finansal Rapor", case=False, na=False)
    1326        ]
    1327
    1328        if financial_reports.empty:
    1329            return pd.DataFrame(
    1330                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1331            )
    1332
    1333        # Use EndDate as the earnings date (latest expected date)
    1334        earnings_dates = []
    1335        for _, row in financial_reports.iterrows():
    1336            end_date = row.get("EndDate", "")
    1337            if end_date:
    1338                try:
    1339                    # Parse Turkish date format (DD.MM.YYYY)
    1340                    parsed = datetime.strptime(end_date, "%d.%m.%Y")
    1341                    earnings_dates.append(parsed)
    1342                except ValueError:
    1343                    continue
    1344
    1345        if not earnings_dates:
    1346            return pd.DataFrame(
    1347                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1348            )
    1349
    1350        result = pd.DataFrame(
    1351            {
    1352                "EPS Estimate": [None] * len(earnings_dates),
    1353                "Reported EPS": [None] * len(earnings_dates),
    1354                "Surprise(%)": [None] * len(earnings_dates),
    1355            },
    1356            index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"),
    1357        )
    1358        result = result.sort_index()
    1359        return result
    1360
    1361    def _parse_date(self, date: str | datetime) -> datetime:
    1362        """Parse a date string to datetime."""
    1363        if isinstance(date, datetime):
    1364            return date
    1365        # Try common formats
    1366        for fmt in ["%Y-%m-%d", "%Y/%m/%d", "%d-%m-%Y", "%d/%m/%Y"]:
    1367            try:
    1368                return datetime.strptime(date, fmt)
    1369            except ValueError:
    1370                continue
    1371        raise ValueError(f"Could not parse date: {date}")
    1372
    1373    def _get_ta_symbol_info(self) -> tuple[str, str]:
    1374        """Get TradingView symbol and screener for TA signals.
    1375
    1376        Returns:
    1377            Tuple of (tv_symbol, screener) for TradingView Scanner API.
    1378        """
    1379        return (f"BIST:{self._symbol}", "turkey")
    1380
    1381    def __repr__(self) -> str:
    1382        return f"Ticker('{self._symbol}')"

A yfinance-like interface for Turkish stock data.

Examples:

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

Ticker(symbol: str) View Source

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

Initialize a Ticker object.

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

symbol: str View Source

    544    @property
    545    def symbol(self) -> str:
    546        """Return the ticker symbol."""
    547        return self._symbol

Return the ticker symbol.

fast_info: borsapy.ticker.FastInfo View Source

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

Get fast access to common ticker information.

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

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

Examples:

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

info: borsapy.ticker.EnrichedInfo View Source

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

Get comprehensive ticker information with yfinance-compatible fields.

Returns: EnrichedInfo object providing dict-like access to:

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

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

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

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

Examples:

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

def history( self, period: str = '1mo', interval: str = '1d', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None, actions: bool = False, adjust: bool = True) -> pandas.core.frame.DataFrame: View Source

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

Get historical OHLCV data.

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

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

Examples:

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

dividends: pandas.core.frame.DataFrame View Source

    788    @cached_property
    789    def dividends(self) -> pd.DataFrame:
    790        """
    791        Get dividend history.
    792
    793        Returns:
    794            DataFrame with dividend history:
    795            - Amount: Dividend per share (TL)
    796            - GrossRate: Gross dividend rate (%)
    797            - NetRate: Net dividend rate (%)
    798            - TotalDividend: Total dividend distributed (TL)
    799
    800        Examples:
    801            >>> stock = Ticker("THYAO")
    802            >>> stock.dividends
    803                           Amount  GrossRate  NetRate  TotalDividend
    804            Date
    805            2025-09-02     3.442    344.20   292.57  4750000000.0
    806            2025-06-16     3.442    344.20   292.57  4750000000.0
    807        """
    808        return self._get_isyatirim().get_dividends(self._symbol)

Get dividend history.

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

Examples:

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

splits: pandas.core.frame.DataFrame View Source

    810    @cached_property
    811    def splits(self) -> pd.DataFrame:
    812        """
    813        Get capital increase (split) history.
    814
    815        Note: Turkish market uses capital increases instead of traditional splits.
    816        - RightsIssue: Paid capital increase (bedelli)
    817        - BonusFromCapital: Free shares from capital reserves (bedelsiz iç kaynak)
    818        - BonusFromDividend: Free shares from dividend (bedelsiz temettüden)
    819
    820        Returns:
    821            DataFrame with capital increase history:
    822            - Capital: New capital after increase (TL)
    823            - RightsIssue: Rights issue rate (%)
    824            - BonusFromCapital: Bonus from capital (%)
    825            - BonusFromDividend: Bonus from dividend (%)
    826
    827        Examples:
    828            >>> stock = Ticker("THYAO")
    829            >>> stock.splits
    830                             Capital  RightsIssue  BonusFromCapital  BonusFromDividend
    831            Date
    832            2013-06-26  1380000000.0         0.0             15.00               0.0
    833            2011-07-11  1200000000.0         0.0              0.00              20.0
    834        """
    835        return self._get_isyatirim().get_capital_increases(self._symbol)

Get capital increase (split) history.

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

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

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

Examples:

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

actions: pandas.core.frame.DataFrame View Source

    837    @cached_property
    838    def actions(self) -> pd.DataFrame:
    839        """
    840        Get combined dividends and splits history.
    841
    842        Returns:
    843            DataFrame with combined dividend and split actions:
    844            - Dividends: Dividend per share (TL) or 0
    845            - Splits: Combined split ratio (0 if no split)
    846
    847        Examples:
    848            >>> stock = Ticker("THYAO")
    849            >>> stock.actions
    850                         Dividends  Splits
    851            Date
    852            2025-09-02      3.442    0.0
    853            2013-06-26      0.000   15.0
    854        """
    855        dividends = self.dividends
    856        splits = self.splits
    857
    858        # Merge on index (Date)
    859        if dividends.empty and splits.empty:
    860            return pd.DataFrame(columns=["Dividends", "Splits"])
    861
    862        # Extract relevant columns
    863        div_series = dividends["Amount"] if not dividends.empty else pd.Series(dtype=float)
    864        split_series = (
    865            splits["BonusFromCapital"] + splits["BonusFromDividend"]
    866            if not splits.empty
    867            else pd.Series(dtype=float)
    868        )
    869
    870        # Combine into single DataFrame
    871        result = pd.DataFrame({"Dividends": div_series, "Splits": split_series})
    872        result = result.fillna(0)
    873        result = result.sort_index(ascending=False)
    874
    875        return result

Get combined dividends and splits history.

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

Examples:

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

def get_balance_sheet( self, quarterly: bool = False, financial_group: str | None = None, last_n: int | str | None = None) -> pandas.core.frame.DataFrame: View Source

    877    def get_balance_sheet(
    878        self,
    879        quarterly: bool = False,
    880        financial_group: str | None = None,
    881        last_n: int | str | None = None,
    882    ) -> pd.DataFrame:
    883        """
    884        Get balance sheet data.
    885
    886        Args:
    887            quarterly: If True, return quarterly data. If False, return annual.
    888            financial_group: Financial group code. Use "UFRS" for banks,
    889                           "XI_29" for industrial companies. If None, defaults to XI_29.
    890            last_n: Number of periods to fetch. None for default (5), int for exact
    891                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
    892
    893        Returns:
    894            DataFrame with balance sheet items as rows and periods as columns.
    895
    896        Examples:
    897            >>> stock = bp.Ticker("THYAO")
    898            >>> stock.get_balance_sheet()  # Annual, industrial (5 periods)
    899            >>> stock.get_balance_sheet(quarterly=True, last_n=20)  # 20 quarters
    900
    901            >>> bank = bp.Ticker("AKBNK")
    902            >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all")
    903        """
    904        return self._get_isyatirim().get_financial_statements(
    905            symbol=self._symbol,
    906            statement_type="balance_sheet",
    907            quarterly=quarterly,
    908            financial_group=financial_group,
    909            last_n=last_n,
    910        )

Get balance sheet data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.

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

Examples:

> > > stock = bp.Ticker("THYAO") stock.get_balance_sheet() # Annual, industrial (5 periods) stock.get_balance_sheet(quarterly=True, last_n=20) # 20 quarters

> > >     >>> bank = bp.Ticker("AKBNK")
> > >     >>> bank.get_balance_sheet(financial_group="UFRS", last_n="all")

def get_income_stmt( self, quarterly: bool = False, financial_group: str | None = None, last_n: int | str | None = None) -> pandas.core.frame.DataFrame: View Source

    912    def get_income_stmt(
    913        self,
    914        quarterly: bool = False,
    915        financial_group: str | None = None,
    916        last_n: int | str | None = None,
    917    ) -> pd.DataFrame:
    918        """
    919        Get income statement data.
    920
    921        Args:
    922            quarterly: If True, return quarterly data. If False, return annual.
    923            financial_group: Financial group code. Use "UFRS" for banks,
    924                           "XI_29" for industrial companies. If None, defaults to XI_29.
    925            last_n: Number of periods to fetch. None for default (5), int for exact
    926                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
    927
    928        Returns:
    929            DataFrame with income statement items as rows and periods as columns.
    930
    931        Examples:
    932            >>> stock = bp.Ticker("THYAO")
    933            >>> stock.get_income_stmt()  # Annual (5 periods)
    934            >>> stock.get_income_stmt(quarterly=True, last_n=20)  # 20 quarters
    935
    936            >>> bank = bp.Ticker("AKBNK")
    937            >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")
    938        """
    939        return self._get_isyatirim().get_financial_statements(
    940            symbol=self._symbol,
    941            statement_type="income_stmt",
    942            quarterly=quarterly,
    943            financial_group=financial_group,
    944            last_n=last_n,
    945        )

Get income statement data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.

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

Examples:

> > > stock = bp.Ticker("THYAO") stock.get_income_stmt() # Annual (5 periods) stock.get_income_stmt(quarterly=True, last_n=20) # 20 quarters

> > >     >>> bank = bp.Ticker("AKBNK")
> > >     >>> bank.get_income_stmt(quarterly=True, financial_group="UFRS")

def get_cashflow( self, quarterly: bool = False, financial_group: str | None = None, last_n: int | str | None = None) -> pandas.core.frame.DataFrame: View Source

    947    def get_cashflow(
    948        self,
    949        quarterly: bool = False,
    950        financial_group: str | None = None,
    951        last_n: int | str | None = None,
    952    ) -> pd.DataFrame:
    953        """
    954        Get cash flow statement data.
    955
    956        Args:
    957            quarterly: If True, return quarterly data. If False, return annual.
    958            financial_group: Financial group code. Use "UFRS" for banks,
    959                           "XI_29" for industrial companies. If None, defaults to XI_29.
    960            last_n: Number of periods to fetch. None for default (5), int for exact
    961                    count (e.g. 10 = 10 annual periods), "all" for maximum available.
    962
    963        Returns:
    964            DataFrame with cash flow items as rows and periods as columns.
    965
    966        Examples:
    967            >>> stock = bp.Ticker("THYAO")
    968            >>> stock.get_cashflow()  # Annual (5 periods)
    969            >>> stock.get_cashflow(quarterly=True, last_n=20)  # 20 quarters
    970
    971            >>> bank = bp.Ticker("AKBNK")
    972            >>> bank.get_cashflow(financial_group="UFRS", last_n="all")
    973        """
    974        return self._get_isyatirim().get_financial_statements(
    975            symbol=self._symbol,
    976            statement_type="cashflow",
    977            quarterly=quarterly,
    978            financial_group=financial_group,
    979            last_n=last_n,
    980        )

Get cash flow statement data.

Args: quarterly: If True, return quarterly data. If False, return annual. financial_group: Financial group code. Use "UFRS" for banks, "XI_29" for industrial companies. If None, defaults to XI_29. last_n: Number of periods to fetch. None for default (5), int for exact count (e.g. 10 = 10 annual periods), "all" for maximum available.

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

Examples:

> > > stock = bp.Ticker("THYAO") stock.get_cashflow() # Annual (5 periods) stock.get_cashflow(quarterly=True, last_n=20) # 20 quarters

> > >     >>> bank = bp.Ticker("AKBNK")
> > >     >>> bank.get_cashflow(financial_group="UFRS", last_n="all")

balance_sheet: pandas.core.frame.DataFrame View Source

    983    @cached_property
    984    def balance_sheet(self) -> pd.DataFrame:
    985        """Annual balance sheet (use get_balance_sheet() for more options)."""
    986        return self.get_balance_sheet(quarterly=False)

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

quarterly_balance_sheet: pandas.core.frame.DataFrame View Source

    988    @cached_property
    989    def quarterly_balance_sheet(self) -> pd.DataFrame:
    990        """Quarterly balance sheet (use get_balance_sheet(quarterly=True) for more options)."""
    991        return self.get_balance_sheet(quarterly=True)

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

income_stmt: pandas.core.frame.DataFrame View Source

    993    @cached_property
    994    def income_stmt(self) -> pd.DataFrame:
    995        """Annual income statement (use get_income_stmt() for more options)."""
    996        return self.get_income_stmt(quarterly=False)

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

quarterly_income_stmt: pandas.core.frame.DataFrame View Source

     998    @cached_property
     999    def quarterly_income_stmt(self) -> pd.DataFrame:
    1000        """Quarterly income statement (use get_income_stmt(quarterly=True) for more options)."""
    1001        return self.get_income_stmt(quarterly=True)

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

cashflow: pandas.core.frame.DataFrame View Source

    1003    @cached_property
    1004    def cashflow(self) -> pd.DataFrame:
    1005        """Annual cash flow (use get_cashflow() for more options)."""
    1006        return self.get_cashflow(quarterly=False)

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

quarterly_cashflow: pandas.core.frame.DataFrame View Source

    1008    @cached_property
    1009    def quarterly_cashflow(self) -> pd.DataFrame:
    1010        """Quarterly cash flow (use get_cashflow(quarterly=True) for more options)."""
    1011        return self.get_cashflow(quarterly=True)

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

def get_ttm_income_stmt(self, financial_group: str | None = None) -> pandas.core.frame.DataFrame: View Source

    1034    def get_ttm_income_stmt(self, financial_group: str | None = None) -> pd.DataFrame:
    1035        """
    1036        Get trailing twelve months (TTM) income statement.
    1037
    1038        Calculates TTM by summing the last 4 quarters of income statement data.
    1039
    1040        Args:
    1041            financial_group: Financial group code. Use "UFRS" for banks,
    1042                           "XI_29" for industrial companies. If None, defaults to XI_29.
    1043
    1044        Returns:
    1045            DataFrame with TTM column containing summed values for each line item.
    1046
    1047        Examples:
    1048            >>> stock = bp.Ticker("THYAO")
    1049            >>> stock.get_ttm_income_stmt()
    1050
    1051            >>> bank = bp.Ticker("AKBNK")
    1052            >>> bank.get_ttm_income_stmt(financial_group="UFRS")
    1053        """
    1054        quarterly = self.get_income_stmt(quarterly=True, financial_group=financial_group)
    1055        return self._calculate_ttm(quarterly)

Get trailing twelve months (TTM) income statement.

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

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

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

Examples:

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

> > >     >>> bank = bp.Ticker("AKBNK")
> > >     >>> bank.get_ttm_income_stmt(financial_group="UFRS")

def get_ttm_cashflow(self, financial_group: str | None = None) -> pandas.core.frame.DataFrame: View Source

    1057    def get_ttm_cashflow(self, financial_group: str | None = None) -> pd.DataFrame:
    1058        """
    1059        Get trailing twelve months (TTM) cash flow statement.
    1060
    1061        Calculates TTM by summing the last 4 quarters of cash flow data.
    1062
    1063        Args:
    1064            financial_group: Financial group code. Use "UFRS" for banks,
    1065                           "XI_29" for industrial companies. If None, defaults to XI_29.
    1066
    1067        Returns:
    1068            DataFrame with TTM column containing summed values for each line item.
    1069
    1070        Examples:
    1071            >>> stock = bp.Ticker("THYAO")
    1072            >>> stock.get_ttm_cashflow()
    1073
    1074            >>> bank = bp.Ticker("AKBNK")
    1075            >>> bank.get_ttm_cashflow(financial_group="UFRS")
    1076        """
    1077        quarterly = self.get_cashflow(quarterly=True, financial_group=financial_group)
    1078        return self._calculate_ttm(quarterly)

Get trailing twelve months (TTM) cash flow statement.

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

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

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

Examples:

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

> > >     >>> bank = bp.Ticker("AKBNK")
> > >     >>> bank.get_ttm_cashflow(financial_group="UFRS")

ttm_income_stmt: pandas.core.frame.DataFrame View Source

    1081    @cached_property
    1082    def ttm_income_stmt(self) -> pd.DataFrame:
    1083        """TTM income statement (use get_ttm_income_stmt() for banks)."""
    1084        return self.get_ttm_income_stmt()

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

ttm_cashflow: pandas.core.frame.DataFrame View Source

    1086    @cached_property
    1087    def ttm_cashflow(self) -> pd.DataFrame:
    1088        """TTM cash flow (use get_ttm_cashflow() for banks)."""
    1089        return self.get_ttm_cashflow()

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

major_holders: pandas.core.frame.DataFrame View Source

    1091    @cached_property
    1092    def major_holders(self) -> pd.DataFrame:
    1093        """
    1094        Get major shareholders (ortaklık yapısı).
    1095
    1096        Returns:
    1097            DataFrame with shareholder names and percentages:
    1098            - Index: Holder name
    1099            - Percentage: Ownership percentage (%)
    1100
    1101        Examples:
    1102            >>> stock = Ticker("THYAO")
    1103            >>> stock.major_holders
    1104                                     Percentage
    1105            Holder
    1106            Diğer                        50.88
    1107            Türkiye Varlık Fonu          49.12
    1108        """
    1109        return self._get_isyatirim().get_major_holders(self._symbol)

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

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

Examples:

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

recommendations: dict View Source

    1111    @cached_property
    1112    def recommendations(self) -> dict:
    1113        """
    1114        Get analyst recommendations and target price.
    1115
    1116        Returns:
    1117            Dictionary with:
    1118            - recommendation: Buy/Hold/Sell (AL/TUT/SAT)
    1119            - target_price: Analyst target price (TL)
    1120            - upside_potential: Expected upside (%)
    1121
    1122        Examples:
    1123            >>> stock = Ticker("THYAO")
    1124            >>> stock.recommendations
    1125            {'recommendation': 'AL', 'target_price': 579.99, 'upside_potential': 116.01}
    1126        """
    1127        return self._get_isyatirim().get_recommendations(self._symbol)

Get analyst recommendations and target price.

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

Examples:

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

recommendations_summary: dict[str, int] View Source

    1129    @cached_property
    1130    def recommendations_summary(self) -> dict[str, int]:
    1131        """
    1132        Get analyst recommendation summary with buy/hold/sell counts.
    1133
    1134        Aggregates individual analyst recommendations from hedeffiyat.com.tr
    1135        into yfinance-compatible categories.
    1136
    1137        Returns:
    1138            Dictionary with counts:
    1139            - strongBuy: Strong buy recommendations
    1140            - buy: Buy recommendations (includes "Endeks Üstü Getiri")
    1141            - hold: Hold recommendations (includes "Nötr", "Endekse Paralel")
    1142            - sell: Sell recommendations (includes "Endeks Altı Getiri")
    1143            - strongSell: Strong sell recommendations
    1144
    1145        Examples:
    1146            >>> stock = Ticker("THYAO")
    1147            >>> stock.recommendations_summary
    1148            {'strongBuy': 0, 'buy': 31, 'hold': 0, 'sell': 0, 'strongSell': 0}
    1149        """
    1150        return self._get_hedeffiyat().get_recommendations_summary(self._symbol)

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

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

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

Examples:

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

news: pandas.core.frame.DataFrame View Source

    1152    @cached_property
    1153    def news(self) -> pd.DataFrame:
    1154        """
    1155        Get recent KAP (Kamuyu Aydınlatma Platformu) disclosures for the stock.
    1156
    1157        Fetches directly from KAP - the official disclosure platform for
    1158        publicly traded companies in Turkey.
    1159
    1160        Returns:
    1161            DataFrame with columns:
    1162            - Date: Disclosure date and time
    1163            - Title: Disclosure headline
    1164            - URL: Link to full disclosure on KAP
    1165
    1166        Examples:
    1167            >>> stock = Ticker("THYAO")
    1168            >>> stock.news
    1169                              Date                                         Title                                         URL
    1170            0  29.12.2025 19:21:18  Haber ve Söylentilere İlişkin Açıklama  https://www.kap.org.tr/tr/Bildirim/1530826
    1171            1  29.12.2025 16:11:36  Payların Geri Alınmasına İlişkin Bildirim  https://www.kap.org.tr/tr/Bildirim/1530656
    1172        """
    1173        return self._get_kap().get_disclosures(self._symbol)

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

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

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

Examples:

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

def get_news_content(self, disclosure_id: int | str) -> str | None: View Source

    1175    def get_news_content(self, disclosure_id: int | str) -> str | None:
    1176        """
    1177        Get full HTML content of a KAP disclosure by ID.
    1178
    1179        Args:
    1180            disclosure_id: KAP disclosure ID from news DataFrame URL.
    1181
    1182        Returns:
    1183            Raw HTML content or None if failed.
    1184
    1185        Examples:
    1186            >>> stock = Ticker("THYAO")
    1187            >>> html = stock.get_news_content(1530826)
    1188        """
    1189        return self._get_kap().get_disclosure_content(disclosure_id)

Get full HTML content of a KAP disclosure by ID.

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

Returns: Raw HTML content or None if failed.

Examples:

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

calendar: pandas.core.frame.DataFrame View Source

    1191    @cached_property
    1192    def calendar(self) -> pd.DataFrame:
    1193        """
    1194        Get expected disclosure calendar for the stock from KAP.
    1195
    1196        Returns upcoming expected disclosures like financial reports,
    1197        annual reports, sustainability reports, and corporate governance reports.
    1198
    1199        Returns:
    1200            DataFrame with columns:
    1201            - StartDate: Expected disclosure window start
    1202            - EndDate: Expected disclosure window end
    1203            - Subject: Type of disclosure (e.g., "Finansal Rapor")
    1204            - Period: Report period (e.g., "Yıllık", "3 Aylık")
    1205            - Year: Fiscal year
    1206
    1207        Examples:
    1208            >>> stock = Ticker("THYAO")
    1209            >>> stock.calendar
    1210                  StartDate       EndDate               Subject   Period  Year
    1211            0  01.01.2026  11.03.2026       Finansal Rapor   Yıllık  2025
    1212            1  01.01.2026  11.03.2026    Faaliyet Raporu  Yıllık  2025
    1213            2  01.04.2026  11.05.2026       Finansal Rapor  3 Aylık  2026
    1214        """
    1215        return self._get_kap().get_calendar(self._symbol)

Get expected disclosure calendar for the stock from KAP.

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

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

Examples:

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

isin: str | None View Source

    1217    @cached_property
    1218    def isin(self) -> str | None:
    1219        """
    1220        Get ISIN (International Securities Identification Number) code.
    1221
    1222        ISIN is a 12-character alphanumeric code that uniquely identifies
    1223        a security, standardized by ISO 6166.
    1224
    1225        Returns:
    1226            ISIN code string (e.g., "TRATHYAO91M5") or None if not found.
    1227
    1228        Examples:
    1229            >>> stock = Ticker("THYAO")
    1230            >>> stock.isin
    1231            'TRATHYAO91M5'
    1232        """
    1233        return self._get_isin_provider().get_isin(self._symbol)

Get ISIN (International Securities Identification Number) code.

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

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

Examples:

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

analyst_price_targets: dict[str, float | int | None] View Source

    1235    @cached_property
    1236    def analyst_price_targets(self) -> dict[str, float | int | None]:
    1237        """
    1238        Get analyst price target data from hedeffiyat.com.tr.
    1239
    1240        Returns aggregated price target information from multiple analysts.
    1241
    1242        Returns:
    1243            Dictionary with:
    1244            - current: Current stock price
    1245            - low: Lowest analyst target price
    1246            - high: Highest analyst target price
    1247            - mean: Average target price
    1248            - median: Median target price
    1249            - numberOfAnalysts: Number of analysts covering the stock
    1250
    1251        Examples:
    1252            >>> stock = Ticker("THYAO")
    1253            >>> stock.analyst_price_targets
    1254            {'current': 268.5, 'low': 388.0, 'high': 580.0, 'mean': 474.49,
    1255             'median': 465.0, 'numberOfAnalysts': 19}
    1256        """
    1257        return self._get_hedeffiyat().get_price_targets(self._symbol)

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

Returns aggregated price target information from multiple analysts.

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

Examples:

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

etf_holders: pandas.core.frame.DataFrame View Source

    1259    @property
    1260    def etf_holders(self) -> pd.DataFrame:
    1261        """
    1262        Get international ETFs that hold this stock.
    1263
    1264        Returns data from TradingView showing which ETFs hold this stock,
    1265        including position value, weight, and ETF characteristics.
    1266
    1267        Returns:
    1268            DataFrame with ETF holder information:
    1269            - symbol: ETF ticker symbol
    1270            - exchange: Exchange (AMEX, NASDAQ, LSE, etc.)
    1271            - name: ETF full name
    1272            - market_cap_usd: Position value in USD
    1273            - holding_weight_pct: Weight percentage (0.09 = 0.09%)
    1274            - issuer: ETF issuer (BlackRock, Vanguard, etc.)
    1275            - management: Management style (Passive/Active)
    1276            - focus: Investment focus (Total Market, Emerging Markets, etc.)
    1277            - expense_ratio: Expense ratio (0.09 = 0.09%)
    1278            - aum_usd: Total assets under management (USD)
    1279            - price: Current ETF price
    1280            - change_pct: Change percentage
    1281
    1282        Examples:
    1283            >>> stock = Ticker("ASELS")
    1284            >>> holders = stock.etf_holders
    1285            >>> holders[['symbol', 'name', 'holding_weight_pct']].head()
    1286               symbol                                      name  holding_weight_pct
    1287            0    IEMG  iShares Core MSCI Emerging Markets ETF            0.090686
    1288            1     VWO     Vanguard FTSE Emerging Markets ETF            0.060000
    1289
    1290            >>> print(f"Total ETFs: {len(holders)}")
    1291            Total ETFs: 118
    1292        """
    1293        return self._get_etf_provider().get_etf_holders(self._symbol)

Get international ETFs that hold this stock.

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

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

Examples:

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

> > >     >>> print(f"Total ETFs: {len(holders)}")
> > >     Total ETFs: 118

earnings_dates: pandas.core.frame.DataFrame View Source

    1295    @cached_property
    1296    def earnings_dates(self) -> pd.DataFrame:
    1297        """
    1298        Get upcoming earnings announcement dates.
    1299
    1300        Derived from KAP calendar, showing expected financial report dates.
    1301        Compatible with yfinance earnings_dates format.
    1302
    1303        Returns:
    1304            DataFrame with index as Earnings Date and columns:
    1305            - EPS Estimate: Always None (not available for BIST)
    1306            - Reported EPS: Always None (not available for BIST)
    1307            - Surprise (%): Always None (not available for BIST)
    1308
    1309        Examples:
    1310            >>> stock = Ticker("THYAO")
    1311            >>> stock.earnings_dates
    1312                            EPS Estimate  Reported EPS  Surprise(%)
    1313            Earnings Date
    1314            2026-03-11           None          None         None
    1315            2026-05-11           None          None         None
    1316        """
    1317        cal = self.calendar
    1318        if cal.empty:
    1319            return pd.DataFrame(
    1320                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1321            )
    1322
    1323        # Filter for financial reports only
    1324        financial_reports = cal[
    1325            cal["Subject"].str.contains("Finansal Rapor", case=False, na=False)
    1326        ]
    1327
    1328        if financial_reports.empty:
    1329            return pd.DataFrame(
    1330                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1331            )
    1332
    1333        # Use EndDate as the earnings date (latest expected date)
    1334        earnings_dates = []
    1335        for _, row in financial_reports.iterrows():
    1336            end_date = row.get("EndDate", "")
    1337            if end_date:
    1338                try:
    1339                    # Parse Turkish date format (DD.MM.YYYY)
    1340                    parsed = datetime.strptime(end_date, "%d.%m.%Y")
    1341                    earnings_dates.append(parsed)
    1342                except ValueError:
    1343                    continue
    1344
    1345        if not earnings_dates:
    1346            return pd.DataFrame(
    1347                columns=["EPS Estimate", "Reported EPS", "Surprise(%)"]
    1348            )
    1349
    1350        result = pd.DataFrame(
    1351            {
    1352                "EPS Estimate": [None] * len(earnings_dates),
    1353                "Reported EPS": [None] * len(earnings_dates),
    1354                "Surprise(%)": [None] * len(earnings_dates),
    1355            },
    1356            index=pd.DatetimeIndex(earnings_dates, name="Earnings Date"),
    1357        )
    1358        result = result.sort_index()
    1359        return result

Get upcoming earnings announcement dates.

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

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

Examples:

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

class Tickers: View Source

     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]) View Source

    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] View Source

    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] View Source

    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: View Source

    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, borsapy.twitter.TwitterMixin): View Source

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

A yfinance-like interface for forex and commodity data.

Supported assets:

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

Examples:

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

> > >     >>> gold = bp.FX("gram-altin")
> > >     >>> gold.current
> > >     {'symbol': 'gram-altin', 'last': 2850.50, ...}

FX(asset: str) View Source

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

Initialize an FX object.

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

asset: str View Source

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

Return the asset code.

symbol: str View Source

    157    @property
    158    def symbol(self) -> str:
    159        """Return the asset code (alias for asset)."""
    160        return self._asset

Return the asset code (alias for asset).

current: dict[str, typing.Any] View Source

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

Get current price information.

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

info: dict[str, typing.Any] View Source

    209    @property
    210    def info(self) -> dict[str, Any]:
    211        """Alias for current property (yfinance compatibility)."""
    212        return self.current

Alias for current property (yfinance compatibility).

bank_rates: pandas.core.frame.DataFrame View Source

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

Get exchange rates from all banks.

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

Examples:

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

def bank_rate(self, bank: str) -> dict[str, typing.Any]: View Source

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

Get exchange rate from a specific bank.

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

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

Examples:

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

@staticmethod
def banks() -> list[str]: View Source

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

Get list of supported banks.

Returns: List of bank codes.

Examples:

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

institution_rates: pandas.core.frame.DataFrame View Source

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

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

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

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

Examples:

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

def institution_rate(self, institution: str) -> dict[str, typing.Any]: View Source

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

Get precious metal rate from a specific institution.

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

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

Examples:

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

@staticmethod
def metal_institutions() -> list[str]: View Source

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

Get list of supported precious metal assets for institution rates.

Returns: List of asset codes that support institution_rates.

Examples:

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

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: View Source

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

Get historical OHLC data.

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

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

Examples:

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

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: View Source

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

Get historical OHLC data from a specific institution.

Supports both precious metals and currencies.

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

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

Examples:

> > > # Metal history

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

> > >     >>> # Currency history
> > >     >>> usd = FX("USD")
> > >     >>> usd.institution_history("akbank", period="1mo")
> > >     >>> usd.institution_history("garanti-bbva", period="5d")

class Crypto(borsapy.technical.TechnicalMixin, borsapy.twitter.TwitterMixin): View Source

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

A yfinance-like interface for cryptocurrency data from BtcTurk.

Examples:

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

> > >     >>> eth = bp.Crypto("ETHTRY")
> > >     >>> eth.current['last']
> > >     125000.0

Crypto(pair: str) View Source

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

Initialize a Crypto object.

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

pair: str View Source

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

Return the trading pair.

symbol: str View Source

    54    @property
    55    def symbol(self) -> str:
    56        """Return the trading pair (alias)."""
    57        return self._pair

Return the trading pair (alias).

current: dict[str, typing.Any] View Source

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

Get current ticker information.

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

info: dict[str, typing.Any] View Source

    81    @property
    82    def info(self) -> dict[str, Any]:
    83        """Alias for current property (yfinance compatibility)."""
    84        return self.current

Alias for current property (yfinance compatibility).

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: View Source

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

Get historical OHLCV data.

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

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

Examples:

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

class Fund(borsapy.technical.TechnicalMixin, borsapy.twitter.TwitterMixin): View Source

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

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

Examples:

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

> > >     >>> fund = bp.Fund("TTE")
> > >     >>> fund.info['return_1y']
> > >     45.67

Fund(fund_code: str, fund_type: str | None = None) View Source

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

Initialize a Fund object.

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

Examples:

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

fund_code: str View Source

    63    @property
    64    def fund_code(self) -> str:
    65        """Return the fund code."""
    66        return self._fund_code

Return the fund code.

symbol: str View Source

    68    @property
    69    def symbol(self) -> str:
    70        """Return the fund code (alias)."""
    71        return self._fund_code

Return the fund code (alias).

fund_type: str View Source

    73    @property
    74    def fund_type(self) -> str:
    75        """
    76        Return the fund type ("YAT" or "EMK").
    77
    78        If not explicitly set, auto-detects on first history() or allocation() call.
    79        """
    80        if self._fund_type:
    81            return self._fund_type
    82        if self._detected_fund_type:
    83            return self._detected_fund_type
    84
    85        # Auto-detect by trying history with YAT first, then EMK
    86        self._detect_fund_type()
    87        return self._detected_fund_type or "YAT"

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

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

info: dict[str, typing.Any] View Source

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

Get detailed fund information.

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

detail: dict[str, typing.Any] View Source

    158    @property
    159    def detail(self) -> dict[str, Any]:
    160        """Alias for info property."""
    161        return self.info

Alias for info property.

performance: dict[str, typing.Any] View Source

    163    @property
    164    def performance(self) -> dict[str, Any]:
    165        """
    166        Get fund performance metrics only.
    167
    168        Returns:
    169            Dictionary with performance data:
    170            - daily_return: Daily return
    171            - return_1m, return_3m, return_6m: Period returns
    172            - return_ytd: Year-to-date return
    173            - return_1y, return_3y, return_5y: Annual returns
    174        """
    175        info = self.info
    176        return {
    177            "daily_return": info.get("daily_return"),
    178            "return_1m": info.get("return_1m"),
    179            "return_3m": info.get("return_3m"),
    180            "return_6m": info.get("return_6m"),
    181            "return_ytd": info.get("return_ytd"),
    182            "return_1y": info.get("return_1y"),
    183            "return_3y": info.get("return_3y"),
    184            "return_5y": info.get("return_5y"),
    185        }

Get fund performance metrics only.

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

management_fee: dict[str, typing.Any] View Source

    187    @property
    188    def management_fee(self) -> dict[str, Any]:
    189        """
    190        Get management fee information for this fund.
    191
    192        Returns:
    193            Dictionary with keys:
    194            - applied_fee: Applied annual management fee (%)
    195            - prospectus_fee: Prospectus management fee (%)
    196            - max_expense_ratio: Maximum total expense ratio (%)
    197            - annual_return: Annual return (%)
    198
    199        Examples:
    200            >>> fund = bp.Fund("AAK")
    201            >>> fund.management_fee
    202            {'applied_fee': 1.0, 'prospectus_fee': 2.2, 'max_expense_ratio': 3.65, 'annual_return': 45.5}
    203        """
    204        empty = {
    205            "applied_fee": None,
    206            "prospectus_fee": None,
    207            "max_expense_ratio": None,
    208            "annual_return": None,
    209        }
    210
    211        try:
    212            fees_list = self._provider.get_management_fees(fund_type=self.fund_type)
    213        except Exception:
    214            return empty
    215
    216        for item in fees_list:
    217            if item.get("fund_code") == self._fund_code:
    218                return {
    219                    "applied_fee": item.get("applied_fee"),
    220                    "prospectus_fee": item.get("prospectus_fee"),
    221                    "max_expense_ratio": item.get("max_expense_ratio"),
    222                    "annual_return": item.get("annual_return"),
    223                }
    224
    225        return empty

Get management fee information for this fund.

Returns: Dictionary with keys: - applied_fee: Applied annual management fee (%) - prospectus_fee: Prospectus management fee (%) - max_expense_ratio: Maximum total expense ratio (%) - annual_return: Annual return (%)

Examples:

> > > fund = bp.Fund("AAK") fund.management_fee {'applied_fee': 1.0, 'prospectus_fee': 2.2, 'max_expense_ratio': 3.65, 'annual_return': 45.5}

tax_category: str | None View Source

    227    @property
    228    def tax_category(self) -> str | None:
    229        """
    230        Get the tax category for this fund based on its TEFAS category.
    231
    232        Returns:
    233            Tax category identifier string (e.g., "degisken_karma_doviz",
    234            "pay_senedi_yogun"), or None if the category cannot be determined.
    235
    236        Examples:
    237            >>> fund = bp.Fund("AAK")
    238            >>> fund.tax_category
    239            'borclanma_para_maden'
    240        """
    241        from borsapy.tax import classify_fund_tax_category
    242
    243        info = self.info
    244        category = info.get("category", "") or ""
    245        fund_name = info.get("name", "") or ""
    246        return classify_fund_tax_category(category, fund_name)

Get the tax category for this fund based on its TEFAS category.

Returns: Tax category identifier string (e.g., "degisken_karma_doviz", "pay_senedi_yogun"), or None if the category cannot be determined.

Examples:

> > > fund = bp.Fund("AAK") fund.tax_category 'borclanma_para_maden'

def withholding_tax_rate( self, purchase_date: datetime.datetime | str | None = None, holding_days: int | None = None) -> float | None: View Source

    248    def withholding_tax_rate(
    249        self,
    250        purchase_date: datetime | str | None = None,
    251        holding_days: int | None = None,
    252    ) -> float | None:
    253        """
    254        Get the withholding tax (stopaj) rate for this fund.
    255
    256        Args:
    257            purchase_date: Date of fund purchase. Accepts datetime, date, or
    258                          "YYYY-MM-DD" string. Defaults to today.
    259            holding_days: Number of days held. Relevant for GSYF/GYF funds
    260                         where >730 days qualifies for 0% rate.
    261
    262        Returns:
    263            Tax rate as a decimal (e.g., 0.15 for 15%), or None if the
    264            fund category cannot be determined.
    265
    266        Examples:
    267            >>> fund = bp.Fund("AAK")
    268            >>> fund.withholding_tax_rate("2025-06-01")
    269            0.15
    270            >>> fund.withholding_tax_rate("2025-08-01")
    271            0.175
    272        """
    273        from datetime import date
    274
    275        from borsapy.tax import get_withholding_tax_rate
    276
    277        cat = self.tax_category
    278        if cat is None:
    279            return None
    280        if purchase_date is None:
    281            purchase_date = date.today()
    282        elif isinstance(purchase_date, datetime):
    283            purchase_date = purchase_date.date()
    284        return get_withholding_tax_rate(cat, purchase_date, holding_days)

Get the withholding tax (stopaj) rate for this fund.

Args: purchase_date: Date of fund purchase. Accepts datetime, date, or "YYYY-MM-DD" string. Defaults to today. holding_days: Number of days held. Relevant for GSYF/GYF funds where >730 days qualifies for 0% rate.

Returns: Tax rate as a decimal (e.g., 0.15 for 15%), or None if the fund category cannot be determined.

Examples:

> > > fund = bp.Fund("AAK") fund.withholding_tax_rate("2025-06-01") 0.15 fund.withholding_tax_rate("2025-08-01") 0.175

allocation: pandas.core.frame.DataFrame View Source

    286    @property
    287    def allocation(self) -> pd.DataFrame:
    288        """
    289        Get current portfolio allocation (asset breakdown) for last 7 days.
    290
    291        For longer periods, use allocation_history() method.
    292
    293        Returns:
    294            DataFrame with columns: Date, asset_type, asset_name, weight.
    295
    296        Examples:
    297            >>> fund = Fund("AAK")
    298            >>> fund.allocation
    299                             Date asset_type         asset_name  weight
    300            0 2024-12-20         HS        Hisse Senedi   45.32
    301            1 2024-12-20         DB        Devlet Bonusu  30.15
    302            ...
    303        """
    304        return self._provider.get_allocation(self._fund_code, fund_type=self.fund_type)

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

For longer periods, use allocation_history() method.

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

Examples:

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

def allocation_history( self, period: str = '1mo', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame: View Source

    306    def allocation_history(
    307        self,
    308        period: str = "1mo",
    309        start: datetime | str | None = None,
    310        end: datetime | str | None = None,
    311    ) -> pd.DataFrame:
    312        """
    313        Get historical portfolio allocation (asset breakdown).
    314
    315        Note: TEFAS API supports maximum ~100 days (3 months) of data.
    316
    317        Args:
    318            period: How much data to fetch. Valid periods:
    319                    1d, 5d, 1mo, 3mo (max ~100 days).
    320                    Ignored if start is provided.
    321            start: Start date (string or datetime).
    322            end: End date (string or datetime). Defaults to today.
    323
    324        Returns:
    325            DataFrame with columns: Date, asset_type, asset_name, weight.
    326
    327        Examples:
    328            >>> fund = Fund("AAK")
    329            >>> fund.allocation_history(period="1mo")  # Last month
    330            >>> fund.allocation_history(period="3mo")  # Last 3 months (max)
    331            >>> fund.allocation_history(start="2024-10-01", end="2024-12-31")
    332        """
    333        start_dt = self._parse_date(start) if start else None
    334        end_dt = self._parse_date(end) if end else None
    335
    336        # If no start date, calculate from period
    337        if start_dt is None:
    338            from datetime import timedelta
    339            end_dt = end_dt or datetime.now()
    340            days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90}.get(period, 30)
    341            # Cap at 100 days (API limit)
    342            days = min(days, 100)
    343            start_dt = end_dt - timedelta(days=days)
    344
    345        return self._provider.get_allocation(
    346            fund_code=self._fund_code,
    347            start=start_dt,
    348            end=end_dt,
    349            fund_type=self.fund_type,
    350        )

Get historical portfolio allocation (asset breakdown).

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

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

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

Examples:

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

def history( self, period: str = '1mo', start: datetime.datetime | str | None = None, end: datetime.datetime | str | None = None) -> pandas.core.frame.DataFrame: View Source

    352    def history(
    353        self,
    354        period: str = "1mo",
    355        start: datetime | str | None = None,
    356        end: datetime | str | None = None,
    357    ) -> pd.DataFrame:
    358        """
    359        Get historical price data.
    360
    361        Args:
    362            period: How much data to fetch. Valid periods:
    363                    1d, 5d, 1mo, 3mo, 6mo, 1y.
    364                    Ignored if start is provided.
    365            start: Start date (string or datetime).
    366            end: End date (string or datetime). Defaults to now.
    367
    368        Returns:
    369            DataFrame with columns: Price, FundSize, Investors.
    370            Index is the Date.
    371
    372        Examples:
    373            >>> fund = Fund("AAK")
    374            >>> fund.history(period="1mo")  # Last month
    375            >>> fund.history(period="1y")  # Last year
    376            >>> fund.history(start="2024-01-01", end="2024-06-30")  # Date range
    377        """
    378        start_dt = self._parse_date(start) if start else None
    379        end_dt = self._parse_date(end) if end else None
    380
    381        return self._provider.get_history(
    382            fund_code=self._fund_code,
    383            period=period,
    384            start=start_dt,
    385            end=end_dt,
    386            fund_type=self.fund_type,
    387        )

Get historical price data.

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

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

Examples:

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

def sharpe_ratio(self, period: str = '1y', risk_free_rate: float | None = None) -> float: View Source

    400    def sharpe_ratio(self, period: str = "1y", risk_free_rate: float | None = None) -> float:
    401        """
    402        Calculate the Sharpe ratio for the fund.
    403
    404        Sharpe Ratio = (Rp - Rf) / σp
    405        Where:
    406        - Rp = Annualized return of the fund
    407        - Rf = Risk-free rate (default: 10Y government bond yield)
    408        - σp = Annualized standard deviation of returns
    409
    410        Args:
    411            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
    412            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
    413                           If None, uses current 10Y bond yield from bp.risk_free_rate().
    414
    415        Returns:
    416            Sharpe ratio as float. Higher is better (>1 good, >2 very good, >3 excellent).
    417
    418        Examples:
    419            >>> fund = bp.Fund("YAY")
    420            >>> fund.sharpe_ratio()  # 1-year Sharpe with current risk-free rate
    421            0.85
    422
    423            >>> fund.sharpe_ratio(period="3y")  # 3-year Sharpe
    424            1.23
    425
    426            >>> fund.sharpe_ratio(risk_free_rate=0.25)  # Custom risk-free rate
    427            0.92
    428        """
    429        metrics = self.risk_metrics(period=period, risk_free_rate=risk_free_rate)
    430        return metrics.get("sharpe_ratio", np.nan)

Calculate the Sharpe ratio for the fund.

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

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

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

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

Examples:

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

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

> > >     >>> fund.sharpe_ratio(risk_free_rate=0.25)  # Custom risk-free rate
> > >     0.92

def risk_metrics( self, period: str = '1y', risk_free_rate: float | None = None) -> dict[str, typing.Any]: View Source

    432    def risk_metrics(
    433        self,
    434        period: str = "1y",
    435        risk_free_rate: float | None = None,
    436    ) -> dict[str, Any]:
    437        """
    438        Calculate comprehensive risk metrics for the fund.
    439
    440        Args:
    441            period: Period for calculation ("1y", "3y", "5y"). Default is "1y".
    442            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
    443                           If None, uses current 10Y bond yield.
    444
    445        Returns:
    446            Dictionary with risk metrics:
    447            - annualized_return: Annualized return (%)
    448            - annualized_volatility: Annualized standard deviation (%)
    449            - sharpe_ratio: Risk-adjusted return (Rp - Rf) / σp
    450            - sortino_ratio: Downside risk-adjusted return
    451            - max_drawdown: Maximum peak-to-trough decline (%)
    452            - risk_free_rate: Risk-free rate used (%)
    453            - trading_days: Number of trading days in the period
    454
    455        Examples:
    456            >>> fund = bp.Fund("YAY")
    457            >>> metrics = fund.risk_metrics()
    458            >>> print(f"Sharpe: {metrics['sharpe_ratio']:.2f}")
    459            >>> print(f"Max Drawdown: {metrics['max_drawdown']:.1f}%")
    460        """
    461        # Get historical data
    462        df = self.history(period=period)
    463
    464        if df.empty or len(df) < 20:
    465            return {
    466                "annualized_return": np.nan,
    467                "annualized_volatility": np.nan,
    468                "sharpe_ratio": np.nan,
    469                "sortino_ratio": np.nan,
    470                "max_drawdown": np.nan,
    471                "risk_free_rate": np.nan,
    472                "trading_days": 0,
    473            }
    474
    475        # Calculate daily returns
    476        prices = df["Price"]
    477        daily_returns = prices.pct_change().dropna()
    478        trading_days = len(daily_returns)
    479
    480        # Annualization factor (trading days per year)
    481        annualization_factor = 252
    482
    483        # Annualized return
    484        total_return = (prices.iloc[-1] / prices.iloc[0]) - 1
    485        years = trading_days / annualization_factor
    486        annualized_return = ((1 + total_return) ** (1 / years) - 1) * 100
    487
    488        # Annualized volatility
    489        daily_volatility = daily_returns.std()
    490        annualized_volatility = daily_volatility * np.sqrt(annualization_factor) * 100
    491
    492        # Get risk-free rate
    493        if risk_free_rate is None:
    494            try:
    495                from borsapy.bond import risk_free_rate as get_rf_rate
    496                rf = get_rf_rate() * 100  # Returns decimal like 0.28, convert to %
    497            except Exception:
    498                rf = 30.0  # Fallback: approximate Turkish 10Y yield
    499        else:
    500            rf = risk_free_rate * 100  # Convert decimal to percentage
    501
    502        # Sharpe Ratio
    503        if annualized_volatility > 0:
    504            sharpe = (annualized_return - rf) / annualized_volatility
    505        else:
    506            sharpe = np.nan
    507
    508        # Sortino Ratio (uses downside deviation)
    509        negative_returns = daily_returns[daily_returns < 0]
    510        if len(negative_returns) > 0:
    511            downside_deviation = negative_returns.std() * np.sqrt(annualization_factor) * 100
    512            if downside_deviation > 0:
    513                sortino = (annualized_return - rf) / downside_deviation
    514            else:
    515                sortino = np.nan
    516        else:
    517            sortino = np.inf  # No negative returns
    518
    519        # Maximum Drawdown
    520        cumulative = (1 + daily_returns).cumprod()
    521        running_max = cumulative.cummax()
    522        drawdowns = (cumulative - running_max) / running_max
    523        max_drawdown = drawdowns.min() * 100  # Negative percentage
    524
    525        return {
    526            "annualized_return": round(annualized_return, 2),
    527            "annualized_volatility": round(annualized_volatility, 2),
    528            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
    529            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
    530            "max_drawdown": round(max_drawdown, 2),
    531            "risk_free_rate": round(rf, 2),
    532            "trading_days": trading_days,
    533        }

Calculate comprehensive risk metrics for the fund.

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

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

Examples:

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

def get_holdings( self, api_key: str, period: str | None = None) -> pandas.core.frame.DataFrame: View Source

    535    def get_holdings(
    536        self,
    537        api_key: str,
    538        period: str | None = None,
    539    ) -> pd.DataFrame:
    540        """
    541        Get detailed portfolio holdings (individual securities).
    542
    543        Returns the specific stocks, ETFs, and funds held by this fund,
    544        with their weights and ISIN codes. Data is sourced from KAP
    545        "Portföy Dağılım Raporu" (Portfolio Distribution Report) disclosures.
    546
    547        Uses OpenRouter LLM for PDF parsing.
    548
    549        Args:
    550            api_key: OpenRouter API key for LLM parsing.
    551                    Get your free API key at: https://openrouter.ai/
    552            period: Optional period in format "YYYY-MM" (e.g., "2025-12").
    553                   If None, returns the most recent holdings.
    554
    555        Returns:
    556            DataFrame with columns:
    557            - symbol: Security symbol (e.g., "GOOGL", "THYAO")
    558            - isin: ISIN code
    559            - name: Full security name
    560            - weight: Portfolio weight (%)
    561            - type: Holding type ('stock', 'etf', 'fund', 'viop', etc.)
    562            - country: Country ('TR', 'US', or None)
    563            - value: Market value in TRY
    564
    565        Raises:
    566            DataNotAvailableError: If holdings data not available.
    567            APIError: If LLM parsing fails.
    568            ImportError: If required packages are not installed.
    569
    570        Examples:
    571            >>> fund = bp.Fund("YAY")
    572            >>> fund.get_holdings(api_key="sk-or-v1-...")
    573               symbol              isin                              name  weight   type country         value
    574            0   GOOGL  US02079K3059             ALPHABET INC CL A    6.76  stock      US  82478088.0
    575            1    AVGO  US11135F1012             BROADCOM INC          5.11  stock      US  62345678.0
    576            ...
    577
    578            >>> # Get holdings for specific period
    579            >>> fund.get_holdings(api_key="sk-or-v1-...", period="2025-12")
    580
    581            >>> # Filter by type
    582            >>> holdings = fund.get_holdings(api_key="sk-or-v1-...")
    583            >>> holdings[holdings['type'] == 'stock']
    584        """
    585        from borsapy._providers.kap_holdings import get_kap_holdings_provider
    586
    587        provider = get_kap_holdings_provider()
    588        return provider.get_holdings_df(self._fund_code, api_key, period=period)

Get detailed portfolio holdings (individual securities).

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

Uses OpenRouter LLM for PDF parsing.

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

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

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

Examples:

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

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

> > >     >>> # Filter by type
> > >     >>> holdings = fund.get_holdings(api_key="sk-or-v1-...")
> > >     >>> holdings[holdings['type'] == 'stock']

class Portfolio(borsapy.technical.TechnicalMixin): View Source

     120class Portfolio(TechnicalMixin):
     121    """
     122    Multi-asset portfolio management with performance tracking and risk metrics.
     123
     124    Supports 4 asset types:
     125    - stock: BIST stocks via Ticker class
     126    - fx: Currencies, metals, commodities via FX class
     127    - crypto: Cryptocurrencies via Crypto class
     128    - fund: TEFAS mutual funds via Fund class
     129
     130    Examples:
     131        >>> import borsapy as bp
     132        >>> p = bp.Portfolio()
     133        >>> p.add("THYAO", shares=100, cost=280)
     134        >>> p.add("gram-altin", shares=5, asset_type="fx")
     135        >>> p.add("YAY", shares=500, asset_type="fund")
     136        >>> p.set_benchmark("XU100")
     137        >>> print(p.holdings)
     138        >>> print(f"Value: {p.value:,.2f} TL")
     139        >>> print(f"Sharpe: {p.risk_metrics()['sharpe_ratio']:.2f}")
     140    """
     141
     142    def __init__(self, benchmark: str = "XU100"):
     143        """
     144        Initialize an empty portfolio.
     145
     146        Args:
     147            benchmark: Index symbol for beta/alpha calculations.
     148                       Default is XU100 (BIST 100).
     149        """
     150        self._holdings: dict[str, Holding] = {}
     151        self._asset_cache: dict[str, Ticker | FX | Crypto | Fund] = {}
     152        self._benchmark = benchmark
     153        self._target_weights: dict[str, float] = {}
     154
     155    # === Asset Management ===
     156
     157    def add(
     158        self,
     159        symbol: str,
     160        shares: float,
     161        cost: float | None = None,
     162        asset_type: str | None = None,
     163        purchase_date: str | date | datetime | None = None,
     164    ) -> "Portfolio":
     165        """
     166        Add an asset to the portfolio.
     167
     168        Args:
     169            symbol: Asset symbol (THYAO, USD, BTCTRY, AAK, etc.)
     170            shares: Number of shares/units.
     171            cost: Cost per share/unit. If None, uses current price.
     172            asset_type: Asset type override. Auto-detected if None.
     173                        Valid values: "stock", "fx", "crypto", "fund"
     174            purchase_date: Date when the asset was purchased.
     175                          Accepts string (YYYY-MM-DD), date, or datetime.
     176                          If None, defaults to today.
     177
     178        Returns:
     179            Self for method chaining.
     180
     181        Examples:
     182            >>> p = Portfolio()
     183            >>> p.add("THYAO", shares=100, cost=280)  # Stock with cost
     184            >>> p.add("GARAN", shares=200)  # Stock at current price
     185            >>> p.add("gram-altin", shares=5, asset_type="fx")  # Metal
     186            >>> p.add("YAY", shares=500, asset_type="fund")  # Mutual fund
     187            >>> p.add("ASELS", shares=50, cost=120, purchase_date="2024-01-15")
     188        """
     189        symbol = symbol.upper() if asset_type != "fx" else symbol
     190
     191        # Detect or validate asset type
     192        if asset_type is None:
     193            detected_type = _detect_asset_type(symbol)
     194        else:
     195            detected_type = asset_type  # type: ignore
     196
     197        # Get current price if cost not provided
     198        if cost is None:
     199            asset = self._get_or_create_asset(symbol, detected_type)
     200            cost = self._get_current_price(asset)
     201
     202        # Parse purchase_date
     203        parsed_date: date | None = None
     204        if purchase_date is not None:
     205            if isinstance(purchase_date, str):
     206                parsed_date = datetime.strptime(purchase_date, "%Y-%m-%d").date()
     207            elif isinstance(purchase_date, datetime):
     208                parsed_date = purchase_date.date()
     209            elif isinstance(purchase_date, date):
     210                parsed_date = purchase_date
     211        else:
     212            parsed_date = date.today()
     213
     214        self._holdings[symbol] = Holding(
     215            symbol=symbol,
     216            shares=shares,
     217            cost_per_share=cost,
     218            asset_type=detected_type,
     219            purchase_date=parsed_date,
     220        )
     221
     222        return self
     223
     224    def remove(self, symbol: str) -> "Portfolio":
     225        """
     226        Remove an asset from the portfolio.
     227
     228        Args:
     229            symbol: Asset symbol to remove.
     230
     231        Returns:
     232            Self for method chaining.
     233        """
     234        symbol_upper = symbol.upper()
     235
     236        # Try both original and uppercase
     237        if symbol in self._holdings:
     238            del self._holdings[symbol]
     239            self._asset_cache.pop(symbol, None)
     240        elif symbol_upper in self._holdings:
     241            del self._holdings[symbol_upper]
     242            self._asset_cache.pop(symbol_upper, None)
     243
     244        return self
     245
     246    def update(
     247        self,
     248        symbol: str,
     249        shares: float | None = None,
     250        cost: float | None = None,
     251    ) -> "Portfolio":
     252        """
     253        Update an existing holding.
     254
     255        Args:
     256            symbol: Asset symbol.
     257            shares: New share count. If None, keeps existing.
     258            cost: New cost per share. If None, keeps existing.
     259
     260        Returns:
     261            Self for method chaining.
     262        """
     263        if symbol not in self._holdings:
     264            symbol = symbol.upper()
     265        if symbol not in self._holdings:
     266            raise KeyError(f"Symbol {symbol} not in portfolio")
     267
     268        holding = self._holdings[symbol]
     269        if shares is not None:
     270            holding.shares = shares
     271        if cost is not None:
     272            holding.cost_per_share = cost
     273
     274        return self
     275
     276    def clear(self) -> "Portfolio":
     277        """
     278        Remove all holdings from the portfolio.
     279
     280        Returns:
     281            Self for method chaining.
     282        """
     283        self._holdings.clear()
     284        self._asset_cache.clear()
     285        return self
     286
     287    def set_benchmark(self, index: str) -> "Portfolio":
     288        """
     289        Set the benchmark index for beta/alpha calculations.
     290
     291        Args:
     292            index: Index symbol (XU100, XU030, XK030, etc.)
     293
     294        Returns:
     295            Self for method chaining.
     296        """
     297        self._benchmark = index
     298        return self
     299
     300    # === Rebalancing ===
     301
     302    def set_target_weights(self, weights: dict[str, float]) -> "Portfolio":
     303        """Set target allocation weights for rebalancing.
     304
     305        Args:
     306            weights: Dict of symbol -> target weight (0.0 to 1.0 scale).
     307                     Must sum to approximately 1.0 (tolerance: 0.01).
     308
     309        Returns:
     310            Self for method chaining.
     311
     312        Raises:
     313            ValueError: If weights don't sum to ~1.0 or contain invalid values.
     314
     315        Examples:
     316            >>> p = Portfolio()
     317            >>> p.add("THYAO", shares=100, cost=280)
     318            >>> p.add("GARAN", shares=200, cost=50)
     319            >>> p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40})
     320        """
     321        total = sum(weights.values())
     322        if abs(total - 1.0) > 0.01:
     323            raise ValueError(
     324                f"Target weights must sum to ~1.0, got {total:.4f}"
     325            )
     326        for symbol, weight in weights.items():
     327            if weight < 0 or weight > 1:
     328                raise ValueError(
     329                    f"Weight for {symbol} must be between 0.0 and 1.0, got {weight}"
     330                )
     331        self._target_weights = dict(weights)
     332        return self
     333
     334    @property
     335    def target_weights(self) -> dict[str, float]:
     336        """Get current target allocation weights."""
     337        return dict(self._target_weights)
     338
     339    def drift(self) -> pd.DataFrame:
     340        """Calculate drift between current and target weights.
     341
     342        Returns:
     343            DataFrame with columns:
     344            - symbol: Asset symbol
     345            - current_weight: Current portfolio weight (0-1 scale)
     346            - target_weight: Target allocation weight (0-1 scale)
     347            - drift: Absolute drift (current - target)
     348            - drift_pct: Drift as percentage points
     349
     350        Raises:
     351            ValueError: If target weights are not set.
     352        """
     353        if not self._target_weights:
     354            raise ValueError(
     355                "Target weights not set. Call set_target_weights() first."
     356            )
     357
     358        current_weights = self.weights  # dict[str, float] on 0-1 scale
     359        rows = []
     360
     361        # Include all symbols from both current holdings and targets
     362        all_symbols = set(current_weights.keys()) | set(self._target_weights.keys())
     363
     364        for symbol in sorted(all_symbols):
     365            current = current_weights.get(symbol, 0.0)
     366            target = self._target_weights.get(symbol, 0.0)
     367            drift_val = current - target
     368            rows.append({
     369                "symbol": symbol,
     370                "current_weight": round(current, 4),
     371                "target_weight": round(target, 4),
     372                "drift": round(drift_val, 4),
     373                "drift_pct": round(drift_val * 100, 2),
     374            })
     375
     376        return pd.DataFrame(rows)
     377
     378    def rebalance_plan(self, threshold: float = 0.0) -> pd.DataFrame:
     379        """Calculate trades needed to rebalance portfolio.
     380
     381        Args:
     382            threshold: Minimum drift (0-1 scale) to trigger a trade.
     383                      E.g., 0.02 = ignore drifts less than 2%.
     384
     385        Returns:
     386            DataFrame with columns:
     387            - symbol: Asset symbol
     388            - current_shares: Current number of shares
     389            - target_shares: Target number of shares
     390            - delta_shares: Shares to buy (+) or sell (-)
     391            - delta_value: Approximate trade value in TL
     392            - action: "BUY", "SELL", or "HOLD"
     393
     394        Raises:
     395            ValueError: If target weights are not set.
     396        """
     397        if not self._target_weights:
     398            raise ValueError(
     399                "Target weights not set. Call set_target_weights() first."
     400            )
     401
     402        total_value = self.value
     403        if total_value == 0:
     404            return pd.DataFrame(
     405                columns=["symbol", "current_shares", "target_shares",
     406                         "delta_shares", "delta_value", "action"]
     407            )
     408
     409        rows = []
     410        all_symbols = set(self._holdings.keys()) | set(self._target_weights.keys())
     411
     412        for symbol in sorted(all_symbols):
     413            holding = self._holdings.get(symbol)
     414            target_weight = self._target_weights.get(symbol, 0.0)
     415
     416            # Get current price
     417            if holding:
     418                asset = self._get_or_create_asset(symbol, holding.asset_type)
     419            else:
     420                # Symbol in targets but not in holdings - detect type
     421                detected_type = _detect_asset_type(symbol)
     422                asset = self._get_or_create_asset(symbol, detected_type)
     423            current_price = self._get_current_price(asset)
     424
     425            current_shares = holding.shares if holding else 0.0
     426            current_value = current_shares * current_price
     427            current_weight = current_value / total_value if total_value else 0.0
     428
     429            # Check threshold
     430            if abs(current_weight - target_weight) < threshold:
     431                rows.append({
     432                    "symbol": symbol,
     433                    "current_shares": current_shares,
     434                    "target_shares": current_shares,
     435                    "delta_shares": 0.0,
     436                    "delta_value": 0.0,
     437                    "action": "HOLD",
     438                })
     439                continue
     440
     441            # Calculate target shares
     442            target_value = total_value * target_weight
     443            if current_price > 0:
     444                target_shares = target_value / current_price
     445            else:
     446                target_shares = 0.0
     447
     448            # Round stock shares to integers
     449            asset_type = holding.asset_type if holding else _detect_asset_type(symbol)
     450            if asset_type == "stock":
     451                target_shares = round(target_shares)
     452
     453            delta_shares = target_shares - current_shares
     454            delta_value = delta_shares * current_price
     455
     456            if delta_shares > 0.001:
     457                action = "BUY"
     458            elif delta_shares < -0.001:
     459                action = "SELL"
     460            else:
     461                action = "HOLD"
     462
     463            rows.append({
     464                "symbol": symbol,
     465                "current_shares": current_shares,
     466                "target_shares": round(target_shares, 4),
     467                "delta_shares": round(delta_shares, 4),
     468                "delta_value": round(delta_value, 2),
     469                "action": action,
     470            })
     471
     472        return pd.DataFrame(rows)
     473
     474    def rebalance(self, threshold: float = 0.0, dry_run: bool = False) -> pd.DataFrame:
     475        """Execute rebalance by updating share counts.
     476
     477        Updates the portfolio holdings to match target weights.
     478        Stock shares are rounded to integers; crypto and fund shares
     479        remain fractional.
     480
     481        Args:
     482            threshold: Minimum drift (0-1 scale) to trigger a trade.
     483            dry_run: If True, return the plan without executing.
     484
     485        Returns:
     486            DataFrame with the rebalance plan (same as rebalance_plan()).
     487
     488        Raises:
     489            ValueError: If target weights are not set.
     490        """
     491        plan = self.rebalance_plan(threshold=threshold)
     492
     493        if dry_run or plan.empty:
     494            return plan
     495
     496        for _, row in plan.iterrows():
     497            symbol = row["symbol"]
     498            action = row["action"]
     499            target_shares = row["target_shares"]
     500
     501            if action == "HOLD":
     502                continue
     503
     504            if symbol in self._holdings:
     505                if target_shares <= 0:
     506                    self.remove(symbol)
     507                else:
     508                    self._holdings[symbol].shares = target_shares
     509            elif target_shares > 0:
     510                # New holding needed
     511                detected_type = _detect_asset_type(symbol)
     512                asset = self._get_or_create_asset(symbol, detected_type)
     513                price = self._get_current_price(asset)
     514                self._holdings[symbol] = Holding(
     515                    symbol=symbol,
     516                    shares=target_shares,
     517                    cost_per_share=price,
     518                    asset_type=detected_type,
     519                    purchase_date=date.today(),
     520                )
     521
     522        return plan
     523
     524    # === Properties ===
     525
     526    @property
     527    def holdings(self) -> pd.DataFrame:
     528        """
     529        Get all holdings as a DataFrame.
     530
     531        Returns:
     532            DataFrame with columns:
     533            - symbol: Asset symbol
     534            - shares: Number of shares
     535            - cost: Cost per share
     536            - current_price: Current price
     537            - value: Current value (shares * price)
     538            - weight: Portfolio weight (%)
     539            - pnl: Profit/loss (TL)
     540            - pnl_pct: Profit/loss (%)
     541            - asset_type: Asset type
     542            - purchase_date: Date when asset was purchased
     543            - holding_days: Number of days since purchase
     544        """
     545        if not self._holdings:
     546            return pd.DataFrame(
     547                columns=[
     548                    "symbol", "shares", "cost", "current_price",
     549                    "value", "weight", "pnl", "pnl_pct", "asset_type",
     550                    "purchase_date", "holding_days"
     551                ]
     552            )
     553
     554        rows = []
     555        total_value = self.value
     556        today = date.today()
     557
     558        for symbol, holding in self._holdings.items():
     559            asset = self._get_or_create_asset(symbol, holding.asset_type)
     560            current_price = self._get_current_price(asset)
     561            value = holding.shares * current_price
     562            cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0
     563            pnl = value - cost_basis if cost_basis else 0
     564            pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0
     565            weight = (value / total_value * 100) if total_value else 0
     566
     567            # Calculate holding days
     568            holding_days = None
     569            if holding.purchase_date:
     570                holding_days = (today - holding.purchase_date).days
     571
     572            rows.append({
     573                "symbol": symbol,
     574                "shares": holding.shares,
     575                "cost": holding.cost_per_share,
     576                "current_price": current_price,
     577                "value": value,
     578                "weight": round(weight, 2),
     579                "pnl": round(pnl, 2),
     580                "pnl_pct": round(pnl_pct, 2),
     581                "asset_type": holding.asset_type,
     582                "purchase_date": holding.purchase_date,
     583                "holding_days": holding_days,
     584            })
     585
     586        return pd.DataFrame(rows)
     587
     588    @property
     589    def symbols(self) -> list[str]:
     590        """Get list of symbols in portfolio."""
     591        return list(self._holdings.keys())
     592
     593    @property
     594    def value(self) -> float:
     595        """Get total portfolio value in TL."""
     596        total = 0.0
     597        for symbol, holding in self._holdings.items():
     598            asset = self._get_or_create_asset(symbol, holding.asset_type)
     599            price = self._get_current_price(asset)
     600            total += holding.shares * price
     601        return total
     602
     603    @property
     604    def cost(self) -> float:
     605        """Get total portfolio cost basis in TL."""
     606        total = 0.0
     607        for holding in self._holdings.values():
     608            if holding.cost_per_share:
     609                total += holding.shares * holding.cost_per_share
     610        return total
     611
     612    @property
     613    def pnl(self) -> float:
     614        """Get total profit/loss in TL."""
     615        return self.value - self.cost
     616
     617    @property
     618    def pnl_pct(self) -> float:
     619        """Get total profit/loss as percentage."""
     620        cost = self.cost
     621        if cost == 0:
     622            return 0.0
     623        return (self.pnl / cost) * 100
     624
     625    @property
     626    def weights(self) -> dict[str, float]:
     627        """Get portfolio weights as dictionary."""
     628        total_value = self.value
     629        if total_value == 0:
     630            return {}
     631
     632        result = {}
     633        for symbol, holding in self._holdings.items():
     634            asset = self._get_or_create_asset(symbol, holding.asset_type)
     635            price = self._get_current_price(asset)
     636            value = holding.shares * price
     637            result[symbol] = round(value / total_value, 4)
     638        return result
     639
     640    # === Performance ===
     641
     642    def history(self, period: str = "1y") -> pd.DataFrame:
     643        """
     644        Get historical portfolio value based on current holdings.
     645
     646        Note: Uses current share counts - does not track historical trades.
     647        When purchase_date is set for a holding, only data from that date
     648        onwards is included in the portfolio value calculation.
     649
     650        Args:
     651            period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).
     652
     653        Returns:
     654            DataFrame with columns: Value, Daily_Return.
     655            Index is Date.
     656        """
     657        if not self._holdings:
     658            return pd.DataFrame(columns=["Value", "Daily_Return"])
     659
     660        all_prices = {}
     661        for symbol, holding in self._holdings.items():
     662            asset = self._get_or_create_asset(symbol, holding.asset_type)
     663            try:
     664                hist = asset.history(period=period)
     665                if hist.empty:
     666                    continue
     667
     668                # Filter by purchase_date if set
     669                if holding.purchase_date:
     670                    # Handle both timezone-aware and timezone-naive indices
     671                    if hasattr(hist.index, 'tz') and hist.index.tz is not None:
     672                        hist = hist[hist.index.date >= holding.purchase_date]
     673                    else:
     674                        hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)]
     675
     676                if hist.empty:
     677                    continue
     678
     679                # Use Close for stocks/index, Price for funds
     680                price_col = "Close" if "Close" in hist.columns else "Price"
     681                all_prices[symbol] = hist[price_col] * holding.shares
     682            except Exception:
     683                continue
     684
     685        if not all_prices:
     686            return pd.DataFrame(columns=["Value", "Daily_Return"])
     687
     688        df = pd.DataFrame(all_prices)
     689        df = df.dropna(how="all")
     690        df["Value"] = df.sum(axis=1)
     691        df["Daily_Return"] = df["Value"].pct_change()
     692        return df[["Value", "Daily_Return"]]
     693
     694    @property
     695    def performance(self) -> dict[str, float]:
     696        """
     697        Get portfolio performance summary.
     698
     699        Returns:
     700            Dictionary with:
     701            - total_return: Total return (%)
     702            - annualized_return: Annualized return (%)
     703            - total_value: Current value (TL)
     704            - total_cost: Total cost (TL)
     705            - total_pnl: Profit/loss (TL)
     706        """
     707        return {
     708            "total_return": self.pnl_pct,
     709            "annualized_return": np.nan,  # Calculated in risk_metrics
     710            "total_value": self.value,
     711            "total_cost": self.cost,
     712            "total_pnl": self.pnl,
     713        }
     714
     715    # === Risk Metrics ===
     716
     717    def risk_metrics(
     718        self,
     719        period: str = "1y",
     720        risk_free_rate: float | None = None,
     721    ) -> dict[str, Any]:
     722        """
     723        Calculate comprehensive risk metrics.
     724
     725        Args:
     726            period: Period for calculation (1y, 3mo, 6mo).
     727            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
     728                           If None, uses current 10Y bond yield.
     729
     730        Returns:
     731            Dictionary with:
     732            - annualized_return: Annualized return (%)
     733            - annualized_volatility: Annualized volatility (%)
     734            - sharpe_ratio: Risk-adjusted return
     735            - sortino_ratio: Downside risk-adjusted return
     736            - max_drawdown: Maximum drawdown (%)
     737            - beta: Beta vs benchmark
     738            - alpha: Alpha vs benchmark (%)
     739            - risk_free_rate: Risk-free rate used (%)
     740            - trading_days: Number of trading days
     741        """
     742        df = self.history(period=period)
     743
     744        if df.empty or len(df) < 20:
     745            return {
     746                "annualized_return": np.nan,
     747                "annualized_volatility": np.nan,
     748                "sharpe_ratio": np.nan,
     749                "sortino_ratio": np.nan,
     750                "max_drawdown": np.nan,
     751                "beta": np.nan,
     752                "alpha": np.nan,
     753                "risk_free_rate": np.nan,
     754                "trading_days": 0,
     755            }
     756
     757        daily_returns = df["Daily_Return"].dropna()
     758        trading_days = len(daily_returns)
     759        annualization = 252
     760
     761        # Annualized return
     762        total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1
     763        years = trading_days / annualization
     764        ann_return = ((1 + total_return) ** (1 / years) - 1) * 100
     765
     766        # Annualized volatility
     767        daily_volatility = daily_returns.std()
     768        ann_volatility = daily_volatility * np.sqrt(annualization) * 100
     769
     770        # Get risk-free rate
     771        if risk_free_rate is None:
     772            try:
     773                from borsapy.bond import risk_free_rate as get_rf_rate
     774                rf = get_rf_rate() * 100  # Convert to percentage
     775            except Exception:
     776                rf = 30.0  # Fallback
     777        else:
     778            rf = risk_free_rate * 100
     779
     780        # Sharpe Ratio
     781        if ann_volatility > 0:
     782            sharpe = (ann_return - rf) / ann_volatility
     783        else:
     784            sharpe = np.nan
     785
     786        # Sortino Ratio (downside deviation)
     787        negative_returns = daily_returns[daily_returns < 0]
     788        if len(negative_returns) > 0:
     789            downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100
     790            if downside_deviation > 0:
     791                sortino = (ann_return - rf) / downside_deviation
     792            else:
     793                sortino = np.nan
     794        else:
     795            sortino = np.inf  # No negative returns
     796
     797        # Maximum Drawdown
     798        cumulative = (1 + daily_returns).cumprod()
     799        running_max = cumulative.cummax()
     800        drawdowns = (cumulative - running_max) / running_max
     801        max_drawdown = drawdowns.min() * 100
     802
     803        # Beta and Alpha (vs benchmark)
     804        beta = np.nan
     805        alpha = np.nan
     806
     807        try:
     808            bench = Index(self._benchmark)
     809            bench_hist = bench.history(period=period)
     810            if not bench_hist.empty:
     811                bench_returns = bench_hist["Close"].pct_change().dropna()
     812
     813                # Align dates
     814                common_dates = daily_returns.index.intersection(bench_returns.index)
     815                if len(common_dates) >= 20:
     816                    port_ret = daily_returns.loc[common_dates]
     817                    bench_ret = bench_returns.loc[common_dates]
     818
     819                    # Beta = Cov(Rp, Rm) / Var(Rm)
     820                    covariance = port_ret.cov(bench_ret)
     821                    variance = bench_ret.var()
     822                    if variance > 0:
     823                        beta = covariance / variance
     824
     825                        # Alpha = Rp - Rf - Beta * (Rm - Rf)
     826                        bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1
     827                        bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100
     828                        alpha = ann_return - rf - beta * (bench_ann - rf)
     829        except Exception:
     830            pass
     831
     832        return {
     833            "annualized_return": round(ann_return, 2),
     834            "annualized_volatility": round(ann_volatility, 2),
     835            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
     836            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
     837            "max_drawdown": round(max_drawdown, 2),
     838            "beta": round(beta, 2) if not np.isnan(beta) else np.nan,
     839            "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan,
     840            "risk_free_rate": round(rf, 2),
     841            "trading_days": trading_days,
     842        }
     843
     844    def sharpe_ratio(self, period: str = "1y") -> float:
     845        """
     846        Calculate Sharpe ratio.
     847
     848        Args:
     849            period: Period for calculation.
     850
     851        Returns:
     852            Sharpe ratio.
     853        """
     854        return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)
     855
     856    def sortino_ratio(self, period: str = "1y") -> float:
     857        """
     858        Calculate Sortino ratio.
     859
     860        Args:
     861            period: Period for calculation.
     862
     863        Returns:
     864            Sortino ratio.
     865        """
     866        return self.risk_metrics(period=period).get("sortino_ratio", np.nan)
     867
     868    def beta(self, benchmark: str | None = None, period: str = "1y") -> float:
     869        """
     870        Calculate beta vs benchmark.
     871
     872        Args:
     873            benchmark: Benchmark index. Uses portfolio default if None.
     874            period: Period for calculation.
     875
     876        Returns:
     877            Beta coefficient.
     878        """
     879        if benchmark:
     880            old_bench = self._benchmark
     881            self._benchmark = benchmark
     882            result = self.risk_metrics(period=period).get("beta", np.nan)
     883            self._benchmark = old_bench
     884            return result
     885        return self.risk_metrics(period=period).get("beta", np.nan)
     886
     887    def correlation_matrix(self, period: str = "1y") -> pd.DataFrame:
     888        """
     889        Calculate correlation matrix between holdings.
     890
     891        Args:
     892            period: Period for calculation.
     893
     894        Returns:
     895            DataFrame with correlation coefficients.
     896        """
     897        if len(self._holdings) < 2:
     898            return pd.DataFrame()
     899
     900        returns_dict = {}
     901        for symbol, holding in self._holdings.items():
     902            try:
     903                asset = self._get_or_create_asset(symbol, holding.asset_type)
     904                hist = asset.history(period=period)
     905                if hist.empty:
     906                    continue
     907                price_col = "Close" if "Close" in hist.columns else "Price"
     908                returns_dict[symbol] = hist[price_col].pct_change()
     909            except Exception:
     910                continue
     911
     912        if len(returns_dict) < 2:
     913            return pd.DataFrame()
     914
     915        df = pd.DataFrame(returns_dict).dropna()
     916        return df.corr()
     917
     918    # === Import/Export ===
     919
     920    def to_dict(self) -> dict[str, Any]:
     921        """
     922        Export portfolio to dictionary.
     923
     924        Returns:
     925            Dictionary with portfolio data.
     926        """
     927        result: dict[str, Any] = {
     928            "benchmark": self._benchmark,
     929            "holdings": [
     930                {
     931                    "symbol": h.symbol,
     932                    "shares": h.shares,
     933                    "cost_per_share": h.cost_per_share,
     934                    "asset_type": h.asset_type,
     935                    "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None,
     936                }
     937                for h in self._holdings.values()
     938            ],
     939        }
     940        if self._target_weights:
     941            result["target_weights"] = dict(self._target_weights)
     942        return result
     943
     944    @classmethod
     945    def from_dict(cls, data: dict[str, Any]) -> "Portfolio":
     946        """
     947        Create portfolio from dictionary.
     948
     949        Args:
     950            data: Dictionary with portfolio data.
     951
     952        Returns:
     953            Portfolio instance.
     954        """
     955        portfolio = cls(benchmark=data.get("benchmark", "XU100"))
     956        for h in data.get("holdings", []):
     957            # Parse purchase_date from ISO string
     958            purchase_date = None
     959            if h.get("purchase_date"):
     960                purchase_date = date.fromisoformat(h["purchase_date"])
     961
     962            portfolio.add(
     963                symbol=h["symbol"],
     964                shares=h["shares"],
     965                cost=h.get("cost_per_share"),
     966                asset_type=h.get("asset_type"),
     967                purchase_date=purchase_date,
     968            )
     969        # Restore target weights if present
     970        if "target_weights" in data:
     971            portfolio._target_weights = dict(data["target_weights"])
     972        return portfolio
     973
     974    # === Private Methods ===
     975
     976    def _get_or_create_asset(
     977        self, symbol: str, asset_type: AssetType
     978    ) -> Ticker | FX | Crypto | Fund:
     979        """Get or create asset instance from cache."""
     980        cache_key = f"{symbol}_{asset_type}"
     981        if cache_key not in self._asset_cache:
     982            self._asset_cache[cache_key] = _get_asset(symbol, asset_type)
     983        return self._asset_cache[cache_key]
     984
     985    def _get_current_price(self, asset: Ticker | FX | Crypto | Fund) -> float:
     986        """Get current price from asset."""
     987        try:
     988            if isinstance(asset, Ticker):
     989                return asset.fast_info.last_price or 0
     990            elif isinstance(asset, Crypto):
     991                return asset.fast_info.last_price or 0
     992            elif isinstance(asset, FX):
     993                current = asset.current
     994                return current.get("last", 0) if current else 0
     995            elif isinstance(asset, Fund):
     996                info = asset.info
     997                return info.get("price", 0) if info else 0
     998        except Exception:
     999            pass
    1000        return 0
    1001
    1002    def __repr__(self) -> str:
    1003        n = len(self._holdings)
    1004        value = self.value
    1005        return f"Portfolio({n} holdings, {value:,.2f} TL)"
    1006
    1007    def __len__(self) -> int:
    1008        return len(self._holdings)

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

Supports 4 asset types:

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

Examples:

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

Portfolio(benchmark: str = 'XU100') View Source

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

Initialize an empty portfolio.

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

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: View Source

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

Add an asset to the portfolio.

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

Returns: Self for method chaining.

Examples:

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

def remove(self, symbol: str) -> Portfolio: View Source

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

Remove an asset from the portfolio.

Args: symbol: Asset symbol to remove.

Returns: Self for method chaining.

def update( self, symbol: str, shares: float | None = None, cost: float | None = None) -> Portfolio: View Source

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

Update an existing holding.

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

Returns: Self for method chaining.

def clear(self) -> Portfolio: View Source

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

Remove all holdings from the portfolio.

Returns: Self for method chaining.

def set_benchmark(self, index: str) -> Portfolio: View Source

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

Set the benchmark index for beta/alpha calculations.

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

Returns: Self for method chaining.

def set_target_weights(self, weights: dict[str, float]) -> Portfolio: View Source

    302    def set_target_weights(self, weights: dict[str, float]) -> "Portfolio":
    303        """Set target allocation weights for rebalancing.
    304
    305        Args:
    306            weights: Dict of symbol -> target weight (0.0 to 1.0 scale).
    307                     Must sum to approximately 1.0 (tolerance: 0.01).
    308
    309        Returns:
    310            Self for method chaining.
    311
    312        Raises:
    313            ValueError: If weights don't sum to ~1.0 or contain invalid values.
    314
    315        Examples:
    316            >>> p = Portfolio()
    317            >>> p.add("THYAO", shares=100, cost=280)
    318            >>> p.add("GARAN", shares=200, cost=50)
    319            >>> p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40})
    320        """
    321        total = sum(weights.values())
    322        if abs(total - 1.0) > 0.01:
    323            raise ValueError(
    324                f"Target weights must sum to ~1.0, got {total:.4f}"
    325            )
    326        for symbol, weight in weights.items():
    327            if weight < 0 or weight > 1:
    328                raise ValueError(
    329                    f"Weight for {symbol} must be between 0.0 and 1.0, got {weight}"
    330                )
    331        self._target_weights = dict(weights)
    332        return self

Set target allocation weights for rebalancing.

Args: weights: Dict of symbol -> target weight (0.0 to 1.0 scale). Must sum to approximately 1.0 (tolerance: 0.01).

Returns: Self for method chaining.

Raises: ValueError: If weights don't sum to ~1.0 or contain invalid values.

Examples:

> > > p = Portfolio() p.add("THYAO", shares=100, cost=280) p.add("GARAN", shares=200, cost=50) p.set_target_weights({"THYAO": 0.60, "GARAN": 0.40})

target_weights: dict[str, float] View Source

    334    @property
    335    def target_weights(self) -> dict[str, float]:
    336        """Get current target allocation weights."""
    337        return dict(self._target_weights)

Get current target allocation weights.

def drift(self) -> pandas.core.frame.DataFrame: View Source

    339    def drift(self) -> pd.DataFrame:
    340        """Calculate drift between current and target weights.
    341
    342        Returns:
    343            DataFrame with columns:
    344            - symbol: Asset symbol
    345            - current_weight: Current portfolio weight (0-1 scale)
    346            - target_weight: Target allocation weight (0-1 scale)
    347            - drift: Absolute drift (current - target)
    348            - drift_pct: Drift as percentage points
    349
    350        Raises:
    351            ValueError: If target weights are not set.
    352        """
    353        if not self._target_weights:
    354            raise ValueError(
    355                "Target weights not set. Call set_target_weights() first."
    356            )
    357
    358        current_weights = self.weights  # dict[str, float] on 0-1 scale
    359        rows = []
    360
    361        # Include all symbols from both current holdings and targets
    362        all_symbols = set(current_weights.keys()) | set(self._target_weights.keys())
    363
    364        for symbol in sorted(all_symbols):
    365            current = current_weights.get(symbol, 0.0)
    366            target = self._target_weights.get(symbol, 0.0)
    367            drift_val = current - target
    368            rows.append({
    369                "symbol": symbol,
    370                "current_weight": round(current, 4),
    371                "target_weight": round(target, 4),
    372                "drift": round(drift_val, 4),
    373                "drift_pct": round(drift_val * 100, 2),
    374            })
    375
    376        return pd.DataFrame(rows)

Calculate drift between current and target weights.

Returns: DataFrame with columns: - symbol: Asset symbol - current_weight: Current portfolio weight (0-1 scale) - target_weight: Target allocation weight (0-1 scale) - drift: Absolute drift (current - target) - drift_pct: Drift as percentage points

Raises: ValueError: If target weights are not set.

def rebalance_plan(self, threshold: float = 0.0) -> pandas.core.frame.DataFrame: View Source

    378    def rebalance_plan(self, threshold: float = 0.0) -> pd.DataFrame:
    379        """Calculate trades needed to rebalance portfolio.
    380
    381        Args:
    382            threshold: Minimum drift (0-1 scale) to trigger a trade.
    383                      E.g., 0.02 = ignore drifts less than 2%.
    384
    385        Returns:
    386            DataFrame with columns:
    387            - symbol: Asset symbol
    388            - current_shares: Current number of shares
    389            - target_shares: Target number of shares
    390            - delta_shares: Shares to buy (+) or sell (-)
    391            - delta_value: Approximate trade value in TL
    392            - action: "BUY", "SELL", or "HOLD"
    393
    394        Raises:
    395            ValueError: If target weights are not set.
    396        """
    397        if not self._target_weights:
    398            raise ValueError(
    399                "Target weights not set. Call set_target_weights() first."
    400            )
    401
    402        total_value = self.value
    403        if total_value == 0:
    404            return pd.DataFrame(
    405                columns=["symbol", "current_shares", "target_shares",
    406                         "delta_shares", "delta_value", "action"]
    407            )
    408
    409        rows = []
    410        all_symbols = set(self._holdings.keys()) | set(self._target_weights.keys())
    411
    412        for symbol in sorted(all_symbols):
    413            holding = self._holdings.get(symbol)
    414            target_weight = self._target_weights.get(symbol, 0.0)
    415
    416            # Get current price
    417            if holding:
    418                asset = self._get_or_create_asset(symbol, holding.asset_type)
    419            else:
    420                # Symbol in targets but not in holdings - detect type
    421                detected_type = _detect_asset_type(symbol)
    422                asset = self._get_or_create_asset(symbol, detected_type)
    423            current_price = self._get_current_price(asset)
    424
    425            current_shares = holding.shares if holding else 0.0
    426            current_value = current_shares * current_price
    427            current_weight = current_value / total_value if total_value else 0.0
    428
    429            # Check threshold
    430            if abs(current_weight - target_weight) < threshold:
    431                rows.append({
    432                    "symbol": symbol,
    433                    "current_shares": current_shares,
    434                    "target_shares": current_shares,
    435                    "delta_shares": 0.0,
    436                    "delta_value": 0.0,
    437                    "action": "HOLD",
    438                })
    439                continue
    440
    441            # Calculate target shares
    442            target_value = total_value * target_weight
    443            if current_price > 0:
    444                target_shares = target_value / current_price
    445            else:
    446                target_shares = 0.0
    447
    448            # Round stock shares to integers
    449            asset_type = holding.asset_type if holding else _detect_asset_type(symbol)
    450            if asset_type == "stock":
    451                target_shares = round(target_shares)
    452
    453            delta_shares = target_shares - current_shares
    454            delta_value = delta_shares * current_price
    455
    456            if delta_shares > 0.001:
    457                action = "BUY"
    458            elif delta_shares < -0.001:
    459                action = "SELL"
    460            else:
    461                action = "HOLD"
    462
    463            rows.append({
    464                "symbol": symbol,
    465                "current_shares": current_shares,
    466                "target_shares": round(target_shares, 4),
    467                "delta_shares": round(delta_shares, 4),
    468                "delta_value": round(delta_value, 2),
    469                "action": action,
    470            })
    471
    472        return pd.DataFrame(rows)

Calculate trades needed to rebalance portfolio.

Args: threshold: Minimum drift (0-1 scale) to trigger a trade. E.g., 0.02 = ignore drifts less than 2%.

Returns: DataFrame with columns: - symbol: Asset symbol - current_shares: Current number of shares - target_shares: Target number of shares - delta_shares: Shares to buy (+) or sell (-) - delta_value: Approximate trade value in TL - action: "BUY", "SELL", or "HOLD"

Raises: ValueError: If target weights are not set.

def rebalance( self, threshold: float = 0.0, dry_run: bool = False) -> pandas.core.frame.DataFrame: View Source

    474    def rebalance(self, threshold: float = 0.0, dry_run: bool = False) -> pd.DataFrame:
    475        """Execute rebalance by updating share counts.
    476
    477        Updates the portfolio holdings to match target weights.
    478        Stock shares are rounded to integers; crypto and fund shares
    479        remain fractional.
    480
    481        Args:
    482            threshold: Minimum drift (0-1 scale) to trigger a trade.
    483            dry_run: If True, return the plan without executing.
    484
    485        Returns:
    486            DataFrame with the rebalance plan (same as rebalance_plan()).
    487
    488        Raises:
    489            ValueError: If target weights are not set.
    490        """
    491        plan = self.rebalance_plan(threshold=threshold)
    492
    493        if dry_run or plan.empty:
    494            return plan
    495
    496        for _, row in plan.iterrows():
    497            symbol = row["symbol"]
    498            action = row["action"]
    499            target_shares = row["target_shares"]
    500
    501            if action == "HOLD":
    502                continue
    503
    504            if symbol in self._holdings:
    505                if target_shares <= 0:
    506                    self.remove(symbol)
    507                else:
    508                    self._holdings[symbol].shares = target_shares
    509            elif target_shares > 0:
    510                # New holding needed
    511                detected_type = _detect_asset_type(symbol)
    512                asset = self._get_or_create_asset(symbol, detected_type)
    513                price = self._get_current_price(asset)
    514                self._holdings[symbol] = Holding(
    515                    symbol=symbol,
    516                    shares=target_shares,
    517                    cost_per_share=price,
    518                    asset_type=detected_type,
    519                    purchase_date=date.today(),
    520                )
    521
    522        return plan

Execute rebalance by updating share counts.

Updates the portfolio holdings to match target weights. Stock shares are rounded to integers; crypto and fund shares remain fractional.

Args: threshold: Minimum drift (0-1 scale) to trigger a trade. dry_run: If True, return the plan without executing.

Returns: DataFrame with the rebalance plan (same as rebalance_plan()).

Raises: ValueError: If target weights are not set.

holdings: pandas.core.frame.DataFrame View Source

    526    @property
    527    def holdings(self) -> pd.DataFrame:
    528        """
    529        Get all holdings as a DataFrame.
    530
    531        Returns:
    532            DataFrame with columns:
    533            - symbol: Asset symbol
    534            - shares: Number of shares
    535            - cost: Cost per share
    536            - current_price: Current price
    537            - value: Current value (shares * price)
    538            - weight: Portfolio weight (%)
    539            - pnl: Profit/loss (TL)
    540            - pnl_pct: Profit/loss (%)
    541            - asset_type: Asset type
    542            - purchase_date: Date when asset was purchased
    543            - holding_days: Number of days since purchase
    544        """
    545        if not self._holdings:
    546            return pd.DataFrame(
    547                columns=[
    548                    "symbol", "shares", "cost", "current_price",
    549                    "value", "weight", "pnl", "pnl_pct", "asset_type",
    550                    "purchase_date", "holding_days"
    551                ]
    552            )
    553
    554        rows = []
    555        total_value = self.value
    556        today = date.today()
    557
    558        for symbol, holding in self._holdings.items():
    559            asset = self._get_or_create_asset(symbol, holding.asset_type)
    560            current_price = self._get_current_price(asset)
    561            value = holding.shares * current_price
    562            cost_basis = (holding.shares * holding.cost_per_share) if holding.cost_per_share else 0
    563            pnl = value - cost_basis if cost_basis else 0
    564            pnl_pct = (pnl / cost_basis * 100) if cost_basis else 0
    565            weight = (value / total_value * 100) if total_value else 0
    566
    567            # Calculate holding days
    568            holding_days = None
    569            if holding.purchase_date:
    570                holding_days = (today - holding.purchase_date).days
    571
    572            rows.append({
    573                "symbol": symbol,
    574                "shares": holding.shares,
    575                "cost": holding.cost_per_share,
    576                "current_price": current_price,
    577                "value": value,
    578                "weight": round(weight, 2),
    579                "pnl": round(pnl, 2),
    580                "pnl_pct": round(pnl_pct, 2),
    581                "asset_type": holding.asset_type,
    582                "purchase_date": holding.purchase_date,
    583                "holding_days": holding_days,
    584            })
    585
    586        return pd.DataFrame(rows)

Get all holdings as a DataFrame.

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

symbols: list[str] View Source

    588    @property
    589    def symbols(self) -> list[str]:
    590        """Get list of symbols in portfolio."""
    591        return list(self._holdings.keys())

Get list of symbols in portfolio.

value: float View Source

    593    @property
    594    def value(self) -> float:
    595        """Get total portfolio value in TL."""
    596        total = 0.0
    597        for symbol, holding in self._holdings.items():
    598            asset = self._get_or_create_asset(symbol, holding.asset_type)
    599            price = self._get_current_price(asset)
    600            total += holding.shares * price
    601        return total

Get total portfolio value in TL.

cost: float View Source

    603    @property
    604    def cost(self) -> float:
    605        """Get total portfolio cost basis in TL."""
    606        total = 0.0
    607        for holding in self._holdings.values():
    608            if holding.cost_per_share:
    609                total += holding.shares * holding.cost_per_share
    610        return total

Get total portfolio cost basis in TL.

pnl: float View Source

    612    @property
    613    def pnl(self) -> float:
    614        """Get total profit/loss in TL."""
    615        return self.value - self.cost

Get total profit/loss in TL.

pnl_pct: float View Source

    617    @property
    618    def pnl_pct(self) -> float:
    619        """Get total profit/loss as percentage."""
    620        cost = self.cost
    621        if cost == 0:
    622            return 0.0
    623        return (self.pnl / cost) * 100

Get total profit/loss as percentage.

weights: dict[str, float] View Source

    625    @property
    626    def weights(self) -> dict[str, float]:
    627        """Get portfolio weights as dictionary."""
    628        total_value = self.value
    629        if total_value == 0:
    630            return {}
    631
    632        result = {}
    633        for symbol, holding in self._holdings.items():
    634            asset = self._get_or_create_asset(symbol, holding.asset_type)
    635            price = self._get_current_price(asset)
    636            value = holding.shares * price
    637            result[symbol] = round(value / total_value, 4)
    638        return result

Get portfolio weights as dictionary.

def history(self, period: str = '1y') -> pandas.core.frame.DataFrame: View Source

    642    def history(self, period: str = "1y") -> pd.DataFrame:
    643        """
    644        Get historical portfolio value based on current holdings.
    645
    646        Note: Uses current share counts - does not track historical trades.
    647        When purchase_date is set for a holding, only data from that date
    648        onwards is included in the portfolio value calculation.
    649
    650        Args:
    651            period: Period for historical data (1d, 5d, 1mo, 3mo, 6mo, 1y).
    652
    653        Returns:
    654            DataFrame with columns: Value, Daily_Return.
    655            Index is Date.
    656        """
    657        if not self._holdings:
    658            return pd.DataFrame(columns=["Value", "Daily_Return"])
    659
    660        all_prices = {}
    661        for symbol, holding in self._holdings.items():
    662            asset = self._get_or_create_asset(symbol, holding.asset_type)
    663            try:
    664                hist = asset.history(period=period)
    665                if hist.empty:
    666                    continue
    667
    668                # Filter by purchase_date if set
    669                if holding.purchase_date:
    670                    # Handle both timezone-aware and timezone-naive indices
    671                    if hasattr(hist.index, 'tz') and hist.index.tz is not None:
    672                        hist = hist[hist.index.date >= holding.purchase_date]
    673                    else:
    674                        hist = hist[hist.index >= pd.Timestamp(holding.purchase_date)]
    675
    676                if hist.empty:
    677                    continue
    678
    679                # Use Close for stocks/index, Price for funds
    680                price_col = "Close" if "Close" in hist.columns else "Price"
    681                all_prices[symbol] = hist[price_col] * holding.shares
    682            except Exception:
    683                continue
    684
    685        if not all_prices:
    686            return pd.DataFrame(columns=["Value", "Daily_Return"])
    687
    688        df = pd.DataFrame(all_prices)
    689        df = df.dropna(how="all")
    690        df["Value"] = df.sum(axis=1)
    691        df["Daily_Return"] = df["Value"].pct_change()
    692        return df[["Value", "Daily_Return"]]

Get historical portfolio value based on current holdings.

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

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

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

performance: dict[str, float] View Source

    694    @property
    695    def performance(self) -> dict[str, float]:
    696        """
    697        Get portfolio performance summary.
    698
    699        Returns:
    700            Dictionary with:
    701            - total_return: Total return (%)
    702            - annualized_return: Annualized return (%)
    703            - total_value: Current value (TL)
    704            - total_cost: Total cost (TL)
    705            - total_pnl: Profit/loss (TL)
    706        """
    707        return {
    708            "total_return": self.pnl_pct,
    709            "annualized_return": np.nan,  # Calculated in risk_metrics
    710            "total_value": self.value,
    711            "total_cost": self.cost,
    712            "total_pnl": self.pnl,
    713        }

Get portfolio performance summary.

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

def risk_metrics( self, period: str = '1y', risk_free_rate: float | None = None) -> dict[str, typing.Any]: View Source

    717    def risk_metrics(
    718        self,
    719        period: str = "1y",
    720        risk_free_rate: float | None = None,
    721    ) -> dict[str, Any]:
    722        """
    723        Calculate comprehensive risk metrics.
    724
    725        Args:
    726            period: Period for calculation (1y, 3mo, 6mo).
    727            risk_free_rate: Annual risk-free rate as decimal (e.g., 0.28 for 28%).
    728                           If None, uses current 10Y bond yield.
    729
    730        Returns:
    731            Dictionary with:
    732            - annualized_return: Annualized return (%)
    733            - annualized_volatility: Annualized volatility (%)
    734            - sharpe_ratio: Risk-adjusted return
    735            - sortino_ratio: Downside risk-adjusted return
    736            - max_drawdown: Maximum drawdown (%)
    737            - beta: Beta vs benchmark
    738            - alpha: Alpha vs benchmark (%)
    739            - risk_free_rate: Risk-free rate used (%)
    740            - trading_days: Number of trading days
    741        """
    742        df = self.history(period=period)
    743
    744        if df.empty or len(df) < 20:
    745            return {
    746                "annualized_return": np.nan,
    747                "annualized_volatility": np.nan,
    748                "sharpe_ratio": np.nan,
    749                "sortino_ratio": np.nan,
    750                "max_drawdown": np.nan,
    751                "beta": np.nan,
    752                "alpha": np.nan,
    753                "risk_free_rate": np.nan,
    754                "trading_days": 0,
    755            }
    756
    757        daily_returns = df["Daily_Return"].dropna()
    758        trading_days = len(daily_returns)
    759        annualization = 252
    760
    761        # Annualized return
    762        total_return = (df["Value"].iloc[-1] / df["Value"].iloc[0]) - 1
    763        years = trading_days / annualization
    764        ann_return = ((1 + total_return) ** (1 / years) - 1) * 100
    765
    766        # Annualized volatility
    767        daily_volatility = daily_returns.std()
    768        ann_volatility = daily_volatility * np.sqrt(annualization) * 100
    769
    770        # Get risk-free rate
    771        if risk_free_rate is None:
    772            try:
    773                from borsapy.bond import risk_free_rate as get_rf_rate
    774                rf = get_rf_rate() * 100  # Convert to percentage
    775            except Exception:
    776                rf = 30.0  # Fallback
    777        else:
    778            rf = risk_free_rate * 100
    779
    780        # Sharpe Ratio
    781        if ann_volatility > 0:
    782            sharpe = (ann_return - rf) / ann_volatility
    783        else:
    784            sharpe = np.nan
    785
    786        # Sortino Ratio (downside deviation)
    787        negative_returns = daily_returns[daily_returns < 0]
    788        if len(negative_returns) > 0:
    789            downside_deviation = negative_returns.std() * np.sqrt(annualization) * 100
    790            if downside_deviation > 0:
    791                sortino = (ann_return - rf) / downside_deviation
    792            else:
    793                sortino = np.nan
    794        else:
    795            sortino = np.inf  # No negative returns
    796
    797        # Maximum Drawdown
    798        cumulative = (1 + daily_returns).cumprod()
    799        running_max = cumulative.cummax()
    800        drawdowns = (cumulative - running_max) / running_max
    801        max_drawdown = drawdowns.min() * 100
    802
    803        # Beta and Alpha (vs benchmark)
    804        beta = np.nan
    805        alpha = np.nan
    806
    807        try:
    808            bench = Index(self._benchmark)
    809            bench_hist = bench.history(period=period)
    810            if not bench_hist.empty:
    811                bench_returns = bench_hist["Close"].pct_change().dropna()
    812
    813                # Align dates
    814                common_dates = daily_returns.index.intersection(bench_returns.index)
    815                if len(common_dates) >= 20:
    816                    port_ret = daily_returns.loc[common_dates]
    817                    bench_ret = bench_returns.loc[common_dates]
    818
    819                    # Beta = Cov(Rp, Rm) / Var(Rm)
    820                    covariance = port_ret.cov(bench_ret)
    821                    variance = bench_ret.var()
    822                    if variance > 0:
    823                        beta = covariance / variance
    824
    825                        # Alpha = Rp - Rf - Beta * (Rm - Rf)
    826                        bench_total = (bench_hist["Close"].iloc[-1] / bench_hist["Close"].iloc[0]) - 1
    827                        bench_ann = ((1 + bench_total) ** (1 / years) - 1) * 100
    828                        alpha = ann_return - rf - beta * (bench_ann - rf)
    829        except Exception:
    830            pass
    831
    832        return {
    833            "annualized_return": round(ann_return, 2),
    834            "annualized_volatility": round(ann_volatility, 2),
    835            "sharpe_ratio": round(sharpe, 2) if not np.isnan(sharpe) else np.nan,
    836            "sortino_ratio": round(sortino, 2) if not np.isnan(sortino) and not np.isinf(sortino) else sortino,
    837            "max_drawdown": round(max_drawdown, 2),
    838            "beta": round(beta, 2) if not np.isnan(beta) else np.nan,
    839            "alpha": round(alpha, 2) if not np.isnan(alpha) else np.nan,
    840            "risk_free_rate": round(rf, 2),
    841            "trading_days": trading_days,
    842        }

Calculate comprehensive risk metrics.

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

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

def sharpe_ratio(self, period: str = '1y') -> float: View Source

    844    def sharpe_ratio(self, period: str = "1y") -> float:
    845        """
    846        Calculate Sharpe ratio.
    847
    848        Args:
    849            period: Period for calculation.
    850
    851        Returns:
    852            Sharpe ratio.
    853        """
    854        return self.risk_metrics(period=period).get("sharpe_ratio", np.nan)

Calculate Sharpe ratio.

Args: period: Period for calculation.

Returns: Sharpe ratio.

def sortino_ratio(self, period: str = '1y') -> float: View Source

    856    def sortino_ratio(self, period: str = "1y") -> float:
    857        """
    858        Calculate Sortino ratio.
    859
    860        Args:
    861            period: Period for calculation.
    862
    863        Returns:
    864            Sortino ratio.
    865        """
    866        return self.risk_metrics(period=period).get("sortino_ratio", np.nan)

Calculate Sortino ratio.

Args: period: Period for calculation.

Returns: Sortino ratio.

def beta(self, benchmark: str | None = None, period: str = '1y') -> float: View Source

    868    def beta(self, benchmark: str | None = None, period: str = "1y") -> float:
    869        """
    870        Calculate beta vs benchmark.
    871
    872        Args:
    873            benchmark: Benchmark index. Uses portfolio default if None.
    874            period: Period for calculation.
    875
    876        Returns:
    877            Beta coefficient.
    878        """
    879        if benchmark:
    880            old_bench = self._benchmark
    881            self._benchmark = benchmark
    882            result = self.risk_metrics(period=period).get("beta", np.nan)
    883            self._benchmark = old_bench
    884            return result
    885        return self.risk_metrics(period=period).get("beta", np.nan)

Calculate beta vs benchmark.

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

Returns: Beta coefficient.

def correlation_matrix(self, period: str = '1y') -> pandas.core.frame.DataFrame: View Source

    887    def correlation_matrix(self, period: str = "1y") -> pd.DataFrame:
    888        """
    889        Calculate correlation matrix between holdings.
    890
    891        Args:
    892            period: Period for calculation.
    893
    894        Returns:
    895            DataFrame with correlation coefficients.
    896        """
    897        if len(self._holdings) < 2:
    898            return pd.DataFrame()
    899
    900        returns_dict = {}
    901        for symbol, holding in self._holdings.items():
    902            try:
    903                asset = self._get_or_create_asset(symbol, holding.asset_type)
    904                hist = asset.history(period=period)
    905                if hist.empty:
    906                    continue
    907                price_col = "Close" if "Close" in hist.columns else "Price"
    908                returns_dict[symbol] = hist[price_col].pct_change()
    909            except Exception:
    910                continue
    911
    912        if len(returns_dict) < 2:
    913            return pd.DataFrame()
    914
    915        df = pd.DataFrame(returns_dict).dropna()
    916        return df.corr()

Calculate correlation matrix between holdings.

Args: period: Period for calculation.

Returns: DataFrame with correlation coefficients.

def to_dict(self) -> dict[str, typing.Any]: View Source

    920    def to_dict(self) -> dict[str, Any]:
    921        """
    922        Export portfolio to dictionary.
    923
    924        Returns:
    925            Dictionary with portfolio data.
    926        """
    927        result: dict[str, Any] = {
    928            "benchmark": self._benchmark,
    929            "holdings": [
    930                {
    931                    "symbol": h.symbol,
    932                    "shares": h.shares,
    933                    "cost_per_share": h.cost_per_share,
    934                    "asset_type": h.asset_type,
    935                    "purchase_date": h.purchase_date.isoformat() if h.purchase_date else None,
    936                }
    937                for h in self._holdings.values()
    938            ],
    939        }
    940        if self._target_weights:
    941            result["target_weights"] = dict(self._target_weights)
    942        return result

Export portfolio to dictionary.

Returns: Dictionary with portfolio data.

@classmethod
def from_dict(cls, data: dict[str, typing.Any]) -> Portfolio: View Source

    944    @classmethod
    945    def from_dict(cls, data: dict[str, Any]) -> "Portfolio":
    946        """
    947        Create portfolio from dictionary.
    948
    949        Args:
    950            data: Dictionary with portfolio data.
    951
    952        Returns:
    953            Portfolio instance.
    954        """
    955        portfolio = cls(benchmark=data.get("benchmark", "XU100"))
    956        for h in data.get("holdings", []):
    957            # Parse purchase_date from ISO string
    958            purchase_date = None
    959            if h.get("purchase_date"):
    960                purchase_date = date.fromisoformat(h["purchase_date"])
    961
    962            portfolio.add(
    963                symbol=h["symbol"],
    964                shares=h["shares"],
    965                cost=h.get("cost_per_share"),
    966                asset_type=h.get("asset_type"),
    967                purchase_date=purchase_date,
    968            )
    969        # Restore target weights if present
    970        if "target_weights" in data:
    971            portfolio._target_weights = dict(data["target_weights"])
    972        return portfolio

Create portfolio from dictionary.

Args: data: Dictionary with portfolio data.

Returns: Portfolio instance.

class Index(borsapy.technical.TechnicalMixin): View Source

     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) View Source

    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 View Source

    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] View Source

     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]] View Source

    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] View Source

    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: View Source

    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: View Source

    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: View Source

     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() View Source

    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]: View Source

    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: View Source

    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: View Source

     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]: View Source

    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: View Source

     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() View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

     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 View Source

    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 View Source

    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: View Source

    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: View Source

     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) View Source

    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 View Source

    48    @property
    49    def maturity(self) -> str:
    50        """Return the bond maturity."""
    51        return self._maturity

Return the bond maturity.

name: str View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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] View Source

     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: View Source

     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) View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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: View Source

     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() View Source

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

Initialize TCMB interface.

policy_rate: float | None View Source

    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 View Source

    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 View Source

     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 View Source

    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: View Source

    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: View Source

     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() View Source

    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: View Source

     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: View Source

    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: View Source

    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: View Source

    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: View Source

    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]: View Source

    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: View Source

     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() View Source

    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: View Source

     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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

     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) View Source

    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 View Source

    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] View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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]]: View Source

    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]: View Source

    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: View Source

    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: View Source

    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: View Source

    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]] View Source

    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: View Source

    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]: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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 View Source

    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: View Source

    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: View Source

    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: View Source

    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]]: View Source

    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: View Source

    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: View Source

    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]: View Source

    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]]: View Source

    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: View Source

    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: View Source

     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) View Source

     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 View Source

    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 View Source

    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: View Source

    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: View Source

    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: View Source

    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]: View Source

    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]: View Source

    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]: View Source

    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: View Source

    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: View Source

     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: View Source

    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( query: str, type: str | None = None, exchange: str | None = None, limit: int = 50, full_info: bool = False) -> list[str] | list[dict[str, typing.Any]]: View Source

     26def search(
     27    query: str,
     28    type: str | None = None,
     29    exchange: str | None = None,
     30    limit: int = 50,
     31    full_info: bool = False,
     32) -> list[str] | list[dict[str, Any]]:
     33    """Search for symbols matching the query.
     34
     35    Searches TradingView's symbol database and optionally merges with local
     36    KAP company data for comprehensive BIST coverage.
     37
     38    Args:
     39        query: Search query (e.g., "banka", "enerji", "THY", "gold")
     40        type: Filter by asset type:
     41              - "stock": Stocks/equities
     42              - "forex" or "fx": Forex pairs
     43              - "crypto": Cryptocurrencies
     44              - "index": Market indices
     45              - "futures": Futures contracts
     46              - "fund" or "etf": Funds and ETFs
     47              - "bond": Bonds
     48        exchange: Filter by exchange (e.g., "BIST", "NASDAQ", "NYSE")
     49        limit: Maximum number of results (default 50, max 100)
     50        full_info: If True, return full result dicts; if False, return symbol list
     51
     52    Returns:
     53        If full_info=False (default):
     54            List of symbol strings: ['AKBNK', 'GARAN', ...]
     55
     56        If full_info=True:
     57            List of result dicts:
     58            [
     59                {
     60                    "symbol": "AKBNK",
     61                    "full_name": "BIST:AKBNK",
     62                    "description": "AKBANK T.A.S.",
     63                    "exchange": "BIST",
     64                    "type": "stock",
     65                    "currency": "TRY",
     66                    "country": "TR"
     67                },
     68                ...
     69            ]
     70
     71    Examples:
     72        >>> import borsapy as bp
     73
     74        >>> # Simple search
     75        >>> bp.search("banka")
     76        ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB', ...]
     77
     78        >>> # Search with type filter
     79        >>> bp.search("gold", type="forex")
     80        ['XAUUSD', 'XAUTRY', ...]
     81
     82        >>> # Search with exchange filter
     83        >>> bp.search("THY", exchange="BIST")
     84        ['THYAO']
     85
     86        >>> # Get full info
     87        >>> bp.search("GARAN", full_info=True)
     88        [{'symbol': 'GARAN', 'description': 'TURKIYE GARANTI BANKASI A.S.', ...}]
     89
     90        >>> # Search crypto
     91        >>> bp.search("BTC", type="crypto")
     92        ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...]
     93
     94        >>> # Search indices
     95        >>> bp.search("XU", type="index", exchange="BIST")
     96        ['XU100', 'XU030', 'XU050', ...]
     97
     98    Raises:
     99        ValueError: If query is empty
    100    """
    101    if not query or not query.strip():
    102        raise ValueError("Search query cannot be empty")
    103
    104    from borsapy._providers.tradingview_search import get_search_provider
    105
    106    provider = get_search_provider()
    107
    108    # Search TradingView
    109    results = provider.search(
    110        query=query,
    111        asset_type=type,
    112        exchange=exchange,
    113        limit=limit,
    114    )
    115
    116    # Try to enhance with KAP data for BIST stocks
    117    if not type or type.lower() == "stock":
    118        if not exchange or exchange.upper() == "BIST":
    119            results = _merge_with_kap(results, query)
    120
    121    if full_info:
    122        return results
    123    else:
    124        # Return just symbol list (deduplicated, maintaining order)
    125        seen = set()
    126        symbols = []
    127        for r in results:
    128            sym = r.get("symbol", "")
    129            if sym and sym not in seen:
    130                seen.add(sym)
    131                symbols.append(sym)
    132        return symbols

Search for symbols matching the query.

Searches TradingView's symbol database and optionally merges with local KAP company data for comprehensive BIST coverage.

Args: query: Search query (e.g., "banka", "enerji", "THY", "gold") type: Filter by asset type: - "stock": Stocks/equities - "forex" or "fx": Forex pairs - "crypto": Cryptocurrencies - "index": Market indices - "futures": Futures contracts - "fund" or "etf": Funds and ETFs - "bond": Bonds exchange: Filter by exchange (e.g., "BIST", "NASDAQ", "NYSE") limit: Maximum number of results (default 50, max 100) full_info: If True, return full result dicts; if False, return symbol list

Returns: If full_info=False (default): List of symbol strings: ['AKBNK', 'GARAN', ...]

    If full_info=True:
        List of result dicts:
        [
            {
                "symbol": "AKBNK",
                "full_name": "BIST:AKBNK",
                "description": "AKBANK T.A.S.",
                "exchange": "BIST",
                "type": "stock",
                "currency": "TRY",
                "country": "TR"
            },
            ...
        ]

Examples:

> > > import borsapy as bp

> > >     >>> # Simple search
> > >     >>> bp.search("banka")
> > >     ['AKBNK', 'GARAN', 'ISCTR', 'YKBNK', 'HALKB', ...]

> > >     >>> # Search with type filter
> > >     >>> bp.search("gold", type="forex")
> > >     ['XAUUSD', 'XAUTRY', ...]

> > >     >>> # Search with exchange filter
> > >     >>> bp.search("THY", exchange="BIST")
> > >     ['THYAO']

> > >     >>> # Get full info
> > >     >>> bp.search("GARAN", full_info=True)
> > >     [{'symbol': 'GARAN', 'description': 'TURKIYE GARANTI BANKASI A.S.', ...}]

> > >     >>> # Search crypto
> > >     >>> bp.search("BTC", type="crypto")
> > >     ['BTCUSD', 'BTCTRY', 'BTCUSDT', ...]

> > >     >>> # Search indices
> > >     >>> bp.search("XU", type="index", exchange="BIST")
> > >     ['XU100', 'XU030', 'XU050', ...]

> > > Raises: ValueError: If query is empty

def search_bist(query: str, limit: int = 50) -> list[str]: View Source

    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]: View Source

    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]: View Source

    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]: View Source

    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]: View Source

    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]: View Source

    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]: View Source

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

Get list of supported banks for exchange rates.

Returns: List of bank codes.

Examples:

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

def metal_institutions() -> list[str]: View Source

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

Get list of supported precious metal assets for institution rates.

Returns: List of asset codes that support institution_rates.

Examples:

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

def crypto_pairs(quote: str = 'TRY') -> list[str]: View Source

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

Get list of available cryptocurrency trading pairs.

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

Returns: List of available trading pair symbols.

Examples:

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

def search_funds(query: str, limit: int = 20) -> list[dict[str, typing.Any]]: View Source

    594def search_funds(query: str, limit: int = 20) -> list[dict[str, Any]]:
    595    """
    596    Search for funds by name or code.
    597
    598    Args:
    599        query: Search query (fund code or name)
    600        limit: Maximum number of results
    601
    602    Returns:
    603        List of matching funds with fund_code, name, fund_type, return_1y.
    604
    605    Examples:
    606        >>> import borsapy as bp
    607        >>> bp.search_funds("ak portföy")
    608        [{'fund_code': 'AAK', 'name': 'Ak Portföy...', ...}, ...]
    609        >>> bp.search_funds("TTE")
    610        [{'fund_code': 'TTE', 'name': 'Türkiye...', ...}]
    611    """
    612    provider = get_tefas_provider()
    613    return provider.search(query, limit)

Search for funds by name or code.

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

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

Examples:

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

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: View Source

    616def screen_funds(
    617    fund_type: str = "YAT",
    618    founder: str | None = None,
    619    min_return_1m: float | None = None,
    620    min_return_3m: float | None = None,
    621    min_return_6m: float | None = None,
    622    min_return_ytd: float | None = None,
    623    min_return_1y: float | None = None,
    624    min_return_3y: float | None = None,
    625    limit: int = 50,
    626) -> pd.DataFrame:
    627    """
    628    Screen funds based on fund type and return criteria.
    629
    630    Args:
    631        fund_type: Fund type filter:
    632            - "YAT": Investment Funds (Yatırım Fonları) - default
    633            - "EMK": Pension Funds (Emeklilik Fonları)
    634        founder: Filter by fund management company code (e.g., "AKP", "GPY", "ISP")
    635        min_return_1m: Minimum 1-month return (%)
    636        min_return_3m: Minimum 3-month return (%)
    637        min_return_6m: Minimum 6-month return (%)
    638        min_return_ytd: Minimum year-to-date return (%)
    639        min_return_1y: Minimum 1-year return (%)
    640        min_return_3y: Minimum 3-year return (%)
    641        limit: Maximum number of results (default: 50)
    642
    643    Returns:
    644        DataFrame with funds matching the criteria, sorted by 1-year return.
    645
    646    Examples:
    647        >>> import borsapy as bp
    648        >>> bp.screen_funds(fund_type="EMK")  # All pension funds
    649           fund_code                    name  return_1y  ...
    650
    651        >>> bp.screen_funds(min_return_1y=50)  # Funds with >50% 1Y return
    652           fund_code                    name  return_1y  ...
    653
    654        >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20)
    655           fund_code                    name  return_ytd  ...
    656    """
    657    provider = get_tefas_provider()
    658    results = provider.screen_funds(
    659        fund_type=fund_type,
    660        founder=founder,
    661        min_return_1m=min_return_1m,
    662        min_return_3m=min_return_3m,
    663        min_return_6m=min_return_6m,
    664        min_return_ytd=min_return_ytd,
    665        min_return_1y=min_return_1y,
    666        min_return_3y=min_return_3y,
    667        limit=limit,
    668    )
    669
    670    if not results:
    671        return pd.DataFrame(columns=["fund_code", "name", "fund_type", "return_1y"])
    672
    673    return pd.DataFrame(results)

Screen funds based on fund type and return criteria.

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

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

Examples:

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

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

> > >     >>> bp.screen_funds(fund_type="EMK", min_return_ytd=20)
> > >        fund_code                    name  return_ytd  ...

def compare_funds(fund_codes: list[str]) -> dict[str, typing.Any]: View Source

    676def compare_funds(fund_codes: list[str]) -> dict[str, Any]:
    677    """
    678    Compare multiple funds side by side.
    679
    680    Args:
    681        fund_codes: List of TEFAS fund codes to compare (max 10)
    682
    683    Returns:
    684        Dictionary with:
    685        - funds: List of fund details with performance metrics
    686        - rankings: Ranking by different criteria (by_return_1y, by_return_ytd, by_size, by_risk_asc)
    687        - summary: Aggregate statistics (avg_return_1y, best/worst returns, total_size)
    688
    689    Examples:
    690        >>> import borsapy as bp
    691        >>> result = bp.compare_funds(["AAK", "TTE", "YAF"])
    692        >>> result['rankings']['by_return_1y']
    693        ['TTE', 'YAF', 'AAK']
    694
    695        >>> result['summary']
    696        {'fund_count': 3, 'avg_return_1y': 45.2, 'best_return_1y': 72.1, ...}
    697
    698        >>> for fund in result['funds']:
    699        ...     print(f"{fund['fund_code']}: {fund['return_1y']}%")
    700        AAK: 32.5%
    701        TTE: 72.1%
    702        YAF: 31.0%
    703    """
    704    provider = get_tefas_provider()
    705    return provider.compare_funds(fund_codes)

Compare multiple funds side by side.

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

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

Examples:

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

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

> > >     >>> for fund in result['funds']:
> > >     ...     print(f"{fund['fund_code']}: {fund['return_1y']}%")
> > >     AAK: 32.5%
> > >     TTE: 72.1%
> > >     YAF: 31.0%

def management_fees( fund_type: str = 'YAT', founder: str | None = None) -> pandas.core.frame.DataFrame: View Source

    708def management_fees(
    709    fund_type: str = "YAT",
    710    founder: str | None = None,
    711) -> pd.DataFrame:
    712    """
    713    Get management fee data for all funds.
    714
    715    Args:
    716        fund_type: Fund type filter:
    717            - "YAT": Investment Funds (Yatırım Fonları) - default
    718            - "EMK": Pension Funds (Emeklilik Fonları)
    719        founder: Filter by founder company code (e.g., "AKP", "GPY")
    720
    721    Returns:
    722        DataFrame with columns: fund_code, name, fund_category, founder_code,
    723        applied_fee, prospectus_fee, max_expense_ratio, annual_return.
    724
    725    Examples:
    726        >>> import borsapy as bp
    727        >>> df = bp.management_fees()
    728        >>> df = bp.management_fees(fund_type="EMK")
    729        >>> df = bp.management_fees(founder="AKP")
    730    """
    731    provider = get_tefas_provider()
    732    results = provider.get_management_fees(fund_type=fund_type, founder=founder)
    733
    734    if not results:
    735        return pd.DataFrame(columns=[
    736            "fund_code", "name", "fund_category", "founder_code",
    737            "applied_fee", "prospectus_fee", "max_expense_ratio", "annual_return",
    738        ])
    739
    740    return pd.DataFrame(results)

Get management fee data for all funds.

Args: fund_type: Fund type filter: - "YAT": Investment Funds (Yatırım Fonları) - default - "EMK": Pension Funds (Emeklilik Fonları) founder: Filter by founder company code (e.g., "AKP", "GPY")

Returns: DataFrame with columns: fund_code, name, fund_category, founder_code, applied_fee, prospectus_fee, max_expense_ratio, annual_return.

Examples:

> > > import borsapy as bp df = bp.management_fees() df = bp.management_fees(fund_type="EMK") df = bp.management_fees(founder="AKP")

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: View Source

    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: View Source

    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]]: View Source

    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]]: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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]]: View Source

    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]: View Source

    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]: View Source

    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: View Source

    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() View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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] View Source

    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] View Source

    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: View Source

    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] View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

     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: View Source

     855class TechnicalAnalyzer:
     856    """Technical analysis wrapper for OHLCV DataFrames.
     857
     858    Provides easy access to technical indicators as methods and properties.
     859
     860    Example:
     861        >>> df = stock.history(period="1y")
     862        >>> ta = TechnicalAnalyzer(df)
     863        >>> ta.rsi()  # Returns full RSI series
     864        >>> ta.latest  # Returns dict with latest values of all indicators
     865    """
     866
     867    def __init__(self, df: pd.DataFrame) -> None:
     868        """Initialize with OHLCV DataFrame.
     869
     870        Args:
     871            df: DataFrame with price data (must have at least 'Close' column)
     872        """
     873        self._df = df.copy()
     874        self._has_volume = "Volume" in df.columns
     875        self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])
     876
     877    def sma(self, period: int = 20) -> pd.Series:
     878        """Calculate Simple Moving Average."""
     879        return calculate_sma(self._df, period)
     880
     881    def ema(self, period: int = 20) -> pd.Series:
     882        """Calculate Exponential Moving Average."""
     883        return calculate_ema(self._df, period)
     884
     885    def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series:
     886        """Calculate Tilson T3 Moving Average."""
     887        return calculate_tilson_t3(self._df, period, vfactor)
     888
     889    def rsi(self, period: int = 14) -> pd.Series:
     890        """Calculate Relative Strength Index."""
     891        return calculate_rsi(self._df, period)
     892
     893    def macd(
     894        self, fast: int = 12, slow: int = 26, signal: int = 9
     895    ) -> pd.DataFrame:
     896        """Calculate MACD (line, signal, histogram)."""
     897        return calculate_macd(self._df, fast, slow, signal)
     898
     899    def bollinger_bands(
     900        self, period: int = 20, std_dev: float = 2.0
     901    ) -> pd.DataFrame:
     902        """Calculate Bollinger Bands (upper, middle, lower)."""
     903        return calculate_bollinger_bands(self._df, period, std_dev)
     904
     905    def atr(self, period: int = 14) -> pd.Series:
     906        """Calculate Average True Range."""
     907        return calculate_atr(self._df, period)
     908
     909    def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame:
     910        """Calculate Stochastic Oscillator (%K, %D)."""
     911        return calculate_stochastic(self._df, k_period, d_period)
     912
     913    def obv(self) -> pd.Series:
     914        """Calculate On-Balance Volume."""
     915        return calculate_obv(self._df)
     916
     917    def vwap(self) -> pd.Series:
     918        """Calculate Volume Weighted Average Price."""
     919        return calculate_vwap(self._df)
     920
     921    def adx(self, period: int = 14) -> pd.Series:
     922        """Calculate Average Directional Index."""
     923        return calculate_adx(self._df, period)
     924
     925    def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame:
     926        """Calculate Supertrend indicator.
     927
     928        Args:
     929            atr_period: Period for ATR calculation (default 10)
     930            multiplier: ATR multiplier for bands (default 3.0)
     931
     932        Returns:
     933            DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower
     934        """
     935        return calculate_supertrend(self._df, atr_period, multiplier)
     936
     937    def hhv(self, period: int = 14, column: str = "High") -> pd.Series:
     938        """Calculate Highest High Value (HHV)."""
     939        return calculate_hhv(self._df, period, column)
     940
     941    def llv(self, period: int = 14, column: str = "Low") -> pd.Series:
     942        """Calculate Lowest Low Value (LLV)."""
     943        return calculate_llv(self._df, period, column)
     944
     945    def mom(self, period: int = 10) -> pd.Series:
     946        """Calculate Momentum (MOM)."""
     947        return calculate_mom(self._df, period)
     948
     949    def roc(self, period: int = 10) -> pd.Series:
     950        """Calculate Rate of Change (ROC)."""
     951        return calculate_roc(self._df, period)
     952
     953    def wma(self, period: int = 20) -> pd.Series:
     954        """Calculate Weighted Moving Average (WMA)."""
     955        return calculate_wma(self._df, period)
     956
     957    def dema(self, period: int = 20) -> pd.Series:
     958        """Calculate Double Exponential Moving Average (DEMA)."""
     959        return calculate_dema(self._df, period)
     960
     961    def tema(self, period: int = 20) -> pd.Series:
     962        """Calculate Triple Exponential Moving Average (TEMA)."""
     963        return calculate_tema(self._df, period)
     964
     965    def heikin_ashi(self) -> pd.DataFrame:
     966        """Calculate Heikin Ashi candlestick values.
     967
     968        Returns:
     969            DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
     970        """
     971        from borsapy.charts import calculate_heikin_ashi
     972
     973        return calculate_heikin_ashi(self._df)
     974
     975    def all(self, **kwargs: Any) -> pd.DataFrame:
     976        """Get DataFrame with all applicable indicators added."""
     977        return add_indicators(self._df, **kwargs)
     978
     979    @property
     980    def latest(self) -> dict[str, float]:
     981        """Get latest values of all applicable indicators.
     982
     983        Returns:
     984            Dictionary with indicator names and their latest values
     985        """
     986        result: dict[str, float] = {}
     987
     988        # Always available (need Close or Price)
     989        has_price = "Close" in self._df.columns or "Price" in self._df.columns
     990        if has_price and len(self._df) > 0:
     991            result["sma_20"] = float(self.sma(20).iloc[-1])
     992            result["sma_50"] = float(self.sma(50).iloc[-1])
     993            result["ema_12"] = float(self.ema(12).iloc[-1])
     994            result["ema_26"] = float(self.ema(26).iloc[-1])
     995            result["t3_5"] = float(self.tilson_t3(5).iloc[-1])
     996            result["rsi_14"] = float(self.rsi(14).iloc[-1])
     997
     998            macd_df = self.macd()
     999            result["macd"] = float(macd_df["MACD"].iloc[-1])
    1000            result["macd_signal"] = float(macd_df["Signal"].iloc[-1])
    1001            result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1])
    1002
    1003            bb_df = self.bollinger_bands()
    1004            result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1])
    1005            result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1])
    1006            result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1])
    1007
    1008        # MetaStock indicators (need Close or Price)
    1009        if has_price and len(self._df) > 0:
    1010            result["mom_10"] = float(self.mom(10).iloc[-1])
    1011            result["roc_10"] = float(self.roc(10).iloc[-1])
    1012            result["wma_20"] = float(self.wma(20).iloc[-1])
    1013            result["dema_20"] = float(self.dema(20).iloc[-1])
    1014            result["tema_20"] = float(self.tema(20).iloc[-1])
    1015
    1016        # Need High, Low, Close
    1017        if self._has_hlc and len(self._df) > 0:
    1018            result["atr_14"] = float(self.atr(14).iloc[-1])
    1019            result["adx_14"] = float(self.adx(14).iloc[-1])
    1020            result["hhv_14"] = float(self.hhv(14).iloc[-1])
    1021            result["llv_14"] = float(self.llv(14).iloc[-1])
    1022
    1023            stoch_df = self.stochastic()
    1024            result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1])
    1025            result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1])
    1026
    1027            st_df = self.supertrend()
    1028            result["supertrend"] = float(st_df["Supertrend"].iloc[-1])
    1029            result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1])
    1030
    1031        # Need Volume
    1032        if self._has_volume and len(self._df) > 0:
    1033            result["obv"] = float(self.obv().iloc[-1])
    1034
    1035        # Need HLC + Volume
    1036        if self._has_hlc and self._has_volume and len(self._df) > 0:
    1037            result["vwap"] = float(self.vwap().iloc[-1])
    1038
    1039        # Round all values
    1040        return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}

Technical analysis wrapper for OHLCV DataFrames.

Provides easy access to technical indicators as methods and properties.

Example:

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

TechnicalAnalyzer(df: pandas.core.frame.DataFrame) View Source

    867    def __init__(self, df: pd.DataFrame) -> None:
    868        """Initialize with OHLCV DataFrame.
    869
    870        Args:
    871            df: DataFrame with price data (must have at least 'Close' column)
    872        """
    873        self._df = df.copy()
    874        self._has_volume = "Volume" in df.columns
    875        self._has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])

Initialize with OHLCV DataFrame.

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

def sma(self, period: int = 20) -> pandas.core.series.Series: View Source

    877    def sma(self, period: int = 20) -> pd.Series:
    878        """Calculate Simple Moving Average."""
    879        return calculate_sma(self._df, period)

Calculate Simple Moving Average.

def ema(self, period: int = 20) -> pandas.core.series.Series: View Source

    881    def ema(self, period: int = 20) -> pd.Series:
    882        """Calculate Exponential Moving Average."""
    883        return calculate_ema(self._df, period)

Calculate Exponential Moving Average.

def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pandas.core.series.Series: View Source

    885    def tilson_t3(self, period: int = 5, vfactor: float = 0.7) -> pd.Series:
    886        """Calculate Tilson T3 Moving Average."""
    887        return calculate_tilson_t3(self._df, period, vfactor)

Calculate Tilson T3 Moving Average.

def rsi(self, period: int = 14) -> pandas.core.series.Series: View Source

    889    def rsi(self, period: int = 14) -> pd.Series:
    890        """Calculate Relative Strength Index."""
    891        return calculate_rsi(self._df, period)

Calculate Relative Strength Index.

def macd( self, fast: int = 12, slow: int = 26, signal: int = 9) -> pandas.core.frame.DataFrame: View Source

    893    def macd(
    894        self, fast: int = 12, slow: int = 26, signal: int = 9
    895    ) -> pd.DataFrame:
    896        """Calculate MACD (line, signal, histogram)."""
    897        return calculate_macd(self._df, fast, slow, signal)

Calculate MACD (line, signal, histogram).

def bollinger_bands( self, period: int = 20, std_dev: float = 2.0) -> pandas.core.frame.DataFrame: View Source

    899    def bollinger_bands(
    900        self, period: int = 20, std_dev: float = 2.0
    901    ) -> pd.DataFrame:
    902        """Calculate Bollinger Bands (upper, middle, lower)."""
    903        return calculate_bollinger_bands(self._df, period, std_dev)

Calculate Bollinger Bands (upper, middle, lower).

def atr(self, period: int = 14) -> pandas.core.series.Series: View Source

    905    def atr(self, period: int = 14) -> pd.Series:
    906        """Calculate Average True Range."""
    907        return calculate_atr(self._df, period)

Calculate Average True Range.

def stochastic( self, k_period: int = 14, d_period: int = 3) -> pandas.core.frame.DataFrame: View Source

    909    def stochastic(self, k_period: int = 14, d_period: int = 3) -> pd.DataFrame:
    910        """Calculate Stochastic Oscillator (%K, %D)."""
    911        return calculate_stochastic(self._df, k_period, d_period)

Calculate Stochastic Oscillator (%K, %D).

def obv(self) -> pandas.core.series.Series: View Source

    913    def obv(self) -> pd.Series:
    914        """Calculate On-Balance Volume."""
    915        return calculate_obv(self._df)

Calculate On-Balance Volume.

def vwap(self) -> pandas.core.series.Series: View Source

    917    def vwap(self) -> pd.Series:
    918        """Calculate Volume Weighted Average Price."""
    919        return calculate_vwap(self._df)

Calculate Volume Weighted Average Price.

def adx(self, period: int = 14) -> pandas.core.series.Series: View Source

    921    def adx(self, period: int = 14) -> pd.Series:
    922        """Calculate Average Directional Index."""
    923        return calculate_adx(self._df, period)

Calculate Average Directional Index.

def supertrend( self, atr_period: int = 10, multiplier: float = 3.0) -> pandas.core.frame.DataFrame: View Source

    925    def supertrend(self, atr_period: int = 10, multiplier: float = 3.0) -> pd.DataFrame:
    926        """Calculate Supertrend indicator.
    927
    928        Args:
    929            atr_period: Period for ATR calculation (default 10)
    930            multiplier: ATR multiplier for bands (default 3.0)
    931
    932        Returns:
    933            DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower
    934        """
    935        return calculate_supertrend(self._df, atr_period, multiplier)

Calculate Supertrend indicator.

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

Returns: DataFrame with Supertrend, Supertrend_Direction, Supertrend_Upper, Supertrend_Lower

def hhv( self, period: int = 14, column: str = 'High') -> pandas.core.series.Series: View Source

    937    def hhv(self, period: int = 14, column: str = "High") -> pd.Series:
    938        """Calculate Highest High Value (HHV)."""
    939        return calculate_hhv(self._df, period, column)

Calculate Highest High Value (HHV).

def llv(self, period: int = 14, column: str = 'Low') -> pandas.core.series.Series: View Source

    941    def llv(self, period: int = 14, column: str = "Low") -> pd.Series:
    942        """Calculate Lowest Low Value (LLV)."""
    943        return calculate_llv(self._df, period, column)

Calculate Lowest Low Value (LLV).

def mom(self, period: int = 10) -> pandas.core.series.Series: View Source

    945    def mom(self, period: int = 10) -> pd.Series:
    946        """Calculate Momentum (MOM)."""
    947        return calculate_mom(self._df, period)

Calculate Momentum (MOM).

def roc(self, period: int = 10) -> pandas.core.series.Series: View Source

    949    def roc(self, period: int = 10) -> pd.Series:
    950        """Calculate Rate of Change (ROC)."""
    951        return calculate_roc(self._df, period)

Calculate Rate of Change (ROC).

def wma(self, period: int = 20) -> pandas.core.series.Series: View Source

    953    def wma(self, period: int = 20) -> pd.Series:
    954        """Calculate Weighted Moving Average (WMA)."""
    955        return calculate_wma(self._df, period)

Calculate Weighted Moving Average (WMA).

def dema(self, period: int = 20) -> pandas.core.series.Series: View Source

    957    def dema(self, period: int = 20) -> pd.Series:
    958        """Calculate Double Exponential Moving Average (DEMA)."""
    959        return calculate_dema(self._df, period)

Calculate Double Exponential Moving Average (DEMA).

def tema(self, period: int = 20) -> pandas.core.series.Series: View Source

    961    def tema(self, period: int = 20) -> pd.Series:
    962        """Calculate Triple Exponential Moving Average (TEMA)."""
    963        return calculate_tema(self._df, period)

Calculate Triple Exponential Moving Average (TEMA).

def heikin_ashi(self) -> pandas.core.frame.DataFrame: View Source

    965    def heikin_ashi(self) -> pd.DataFrame:
    966        """Calculate Heikin Ashi candlestick values.
    967
    968        Returns:
    969            DataFrame with HA_Open, HA_High, HA_Low, HA_Close, Volume columns
    970        """
    971        from borsapy.charts import calculate_heikin_ashi
    972
    973        return calculate_heikin_ashi(self._df)

Calculate Heikin Ashi candlestick values.

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

def all(self, **kwargs: Any) -> pandas.core.frame.DataFrame: View Source

    975    def all(self, **kwargs: Any) -> pd.DataFrame:
    976        """Get DataFrame with all applicable indicators added."""
    977        return add_indicators(self._df, **kwargs)

Get DataFrame with all applicable indicators added.

latest: dict[str, float] View Source

     979    @property
     980    def latest(self) -> dict[str, float]:
     981        """Get latest values of all applicable indicators.
     982
     983        Returns:
     984            Dictionary with indicator names and their latest values
     985        """
     986        result: dict[str, float] = {}
     987
     988        # Always available (need Close or Price)
     989        has_price = "Close" in self._df.columns or "Price" in self._df.columns
     990        if has_price and len(self._df) > 0:
     991            result["sma_20"] = float(self.sma(20).iloc[-1])
     992            result["sma_50"] = float(self.sma(50).iloc[-1])
     993            result["ema_12"] = float(self.ema(12).iloc[-1])
     994            result["ema_26"] = float(self.ema(26).iloc[-1])
     995            result["t3_5"] = float(self.tilson_t3(5).iloc[-1])
     996            result["rsi_14"] = float(self.rsi(14).iloc[-1])
     997
     998            macd_df = self.macd()
     999            result["macd"] = float(macd_df["MACD"].iloc[-1])
    1000            result["macd_signal"] = float(macd_df["Signal"].iloc[-1])
    1001            result["macd_histogram"] = float(macd_df["Histogram"].iloc[-1])
    1002
    1003            bb_df = self.bollinger_bands()
    1004            result["bb_upper"] = float(bb_df["BB_Upper"].iloc[-1])
    1005            result["bb_middle"] = float(bb_df["BB_Middle"].iloc[-1])
    1006            result["bb_lower"] = float(bb_df["BB_Lower"].iloc[-1])
    1007
    1008        # MetaStock indicators (need Close or Price)
    1009        if has_price and len(self._df) > 0:
    1010            result["mom_10"] = float(self.mom(10).iloc[-1])
    1011            result["roc_10"] = float(self.roc(10).iloc[-1])
    1012            result["wma_20"] = float(self.wma(20).iloc[-1])
    1013            result["dema_20"] = float(self.dema(20).iloc[-1])
    1014            result["tema_20"] = float(self.tema(20).iloc[-1])
    1015
    1016        # Need High, Low, Close
    1017        if self._has_hlc and len(self._df) > 0:
    1018            result["atr_14"] = float(self.atr(14).iloc[-1])
    1019            result["adx_14"] = float(self.adx(14).iloc[-1])
    1020            result["hhv_14"] = float(self.hhv(14).iloc[-1])
    1021            result["llv_14"] = float(self.llv(14).iloc[-1])
    1022
    1023            stoch_df = self.stochastic()
    1024            result["stoch_k"] = float(stoch_df["Stoch_K"].iloc[-1])
    1025            result["stoch_d"] = float(stoch_df["Stoch_D"].iloc[-1])
    1026
    1027            st_df = self.supertrend()
    1028            result["supertrend"] = float(st_df["Supertrend"].iloc[-1])
    1029            result["supertrend_direction"] = float(st_df["Supertrend_Direction"].iloc[-1])
    1030
    1031        # Need Volume
    1032        if self._has_volume and len(self._df) > 0:
    1033            result["obv"] = float(self.obv().iloc[-1])
    1034
    1035        # Need HLC + Volume
    1036        if self._has_hlc and self._has_volume and len(self._df) > 0:
    1037            result["vwap"] = float(self.vwap().iloc[-1])
    1038
    1039        # Round all values
    1040        return {k: round(v, 4) if not np.isnan(v) else np.nan for k, v in result.items()}

Get latest values of all applicable indicators.

Returns: Dictionary with indicator names and their latest values

def add_indicators( df: pandas.core.frame.DataFrame, indicators: list[str] | None = None, **kwargs: Any) -> pandas.core.frame.DataFrame: View Source

    729def add_indicators(
    730    df: pd.DataFrame,
    731    indicators: list[str] | None = None,
    732    **kwargs: Any,
    733) -> pd.DataFrame:
    734    """Add technical indicator columns to a DataFrame.
    735
    736    Args:
    737        df: DataFrame with OHLCV data (Open, High, Low, Close, Volume)
    738        indicators: List of indicators to add. If None, adds all applicable.
    739            Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr',
    740                     'stochastic', 'obv', 'vwap', 'adx', 'supertrend',
    741                     'hhv', 'llv', 'mom', 'roc', 'wma', 'dema', 'tema'
    742        **kwargs: Additional arguments for specific indicators:
    743            - sma_period: SMA period (default 20)
    744            - ema_period: EMA period (default 12)
    745            - rsi_period: RSI period (default 14)
    746            - bb_period: Bollinger Bands period (default 20)
    747            - atr_period: ATR period (default 14)
    748            - adx_period: ADX period (default 14)
    749            - supertrend_period: Supertrend ATR period (default 10)
    750            - supertrend_multiplier: Supertrend ATR multiplier (default 3.0)
    751            - hhv_period: HHV period (default 14)
    752            - llv_period: LLV period (default 14)
    753            - mom_period: MOM period (default 10)
    754            - roc_period: ROC period (default 10)
    755            - wma_period: WMA period (default 20)
    756            - dema_period: DEMA period (default 20)
    757            - tema_period: TEMA period (default 20)
    758
    759    Returns:
    760        DataFrame with indicator columns added
    761    """
    762    result = df.copy()
    763
    764    # Default indicators based on available columns
    765    has_volume = "Volume" in df.columns
    766    has_hlc = all(col in df.columns for col in ["High", "Low", "Close"])
    767
    768    if indicators is None:
    769        indicators = ["sma", "ema", "rsi", "macd", "bollinger"]
    770        if has_hlc:
    771            indicators.extend(["atr", "stochastic", "adx", "supertrend"])
    772        if has_volume:
    773            indicators.append("obv")
    774        if has_volume and has_hlc:
    775            indicators.append("vwap")
    776
    777    # Get periods from kwargs
    778    sma_period = kwargs.get("sma_period", 20)
    779    ema_period = kwargs.get("ema_period", 12)
    780    rsi_period = kwargs.get("rsi_period", 14)
    781    bb_period = kwargs.get("bb_period", 20)
    782    atr_period = kwargs.get("atr_period", 14)
    783    adx_period = kwargs.get("adx_period", 14)
    784    supertrend_period = kwargs.get("supertrend_period", 10)
    785    supertrend_multiplier = kwargs.get("supertrend_multiplier", 3.0)
    786    hhv_period = kwargs.get("hhv_period", 14)
    787    llv_period = kwargs.get("llv_period", 14)
    788    mom_period = kwargs.get("mom_period", 10)
    789    roc_period = kwargs.get("roc_period", 10)
    790    wma_period = kwargs.get("wma_period", 20)
    791    dema_period = kwargs.get("dema_period", 20)
    792    tema_period = kwargs.get("tema_period", 20)
    793
    794    # Add indicators
    795    for indicator in indicators:
    796        indicator = indicator.lower()
    797
    798        if indicator == "sma":
    799            result[f"SMA_{sma_period}"] = calculate_sma(df, sma_period)
    800        elif indicator == "ema":
    801            result[f"EMA_{ema_period}"] = calculate_ema(df, ema_period)
    802        elif indicator == "rsi":
    803            result[f"RSI_{rsi_period}"] = calculate_rsi(df, rsi_period)
    804        elif indicator == "macd":
    805            macd_df = calculate_macd(df)
    806            result["MACD"] = macd_df["MACD"]
    807            result["MACD_Signal"] = macd_df["Signal"]
    808            result["MACD_Hist"] = macd_df["Histogram"]
    809        elif indicator == "bollinger":
    810            bb_df = calculate_bollinger_bands(df, bb_period)
    811            result["BB_Upper"] = bb_df["BB_Upper"]
    812            result["BB_Middle"] = bb_df["BB_Middle"]
    813            result["BB_Lower"] = bb_df["BB_Lower"]
    814        elif indicator == "atr" and has_hlc:
    815            result[f"ATR_{atr_period}"] = calculate_atr(df, atr_period)
    816        elif indicator == "stochastic" and has_hlc:
    817            stoch_df = calculate_stochastic(df)
    818            result["Stoch_K"] = stoch_df["Stoch_K"]
    819            result["Stoch_D"] = stoch_df["Stoch_D"]
    820        elif indicator == "obv" and has_volume:
    821            result["OBV"] = calculate_obv(df)
    822        elif indicator == "vwap" and has_volume and has_hlc:
    823            result["VWAP"] = calculate_vwap(df)
    824        elif indicator == "adx" and has_hlc:
    825            result[f"ADX_{adx_period}"] = calculate_adx(df, adx_period)
    826        elif indicator == "supertrend" and has_hlc:
    827            st_df = calculate_supertrend(df, supertrend_period, supertrend_multiplier)
    828            result["Supertrend"] = st_df["Supertrend"]
    829            result["Supertrend_Direction"] = st_df["Supertrend_Direction"]
    830        elif indicator == "hhv":
    831            col = "High" if "High" in df.columns else "Close"
    832            result[f"HHV_{hhv_period}"] = calculate_hhv(df, hhv_period, col)
    833        elif indicator == "llv":
    834            col = "Low" if "Low" in df.columns else "Close"
    835            result[f"LLV_{llv_period}"] = calculate_llv(df, llv_period, col)
    836        elif indicator == "mom":
    837            result[f"MOM_{mom_period}"] = calculate_mom(df, mom_period)
    838        elif indicator == "roc":
    839            result[f"ROC_{roc_period}"] = calculate_roc(df, roc_period)
    840        elif indicator == "wma":
    841            result[f"WMA_{wma_period}"] = calculate_wma(df, wma_period)
    842        elif indicator == "dema":
    843            result[f"DEMA_{dema_period}"] = calculate_dema(df, dema_period)
    844        elif indicator == "tema":
    845            result[f"TEMA_{tema_period}"] = calculate_tema(df, tema_period)
    846
    847    return result

Add technical indicator columns to a DataFrame.

Args: df: DataFrame with OHLCV data (Open, High, Low, Close, Volume) indicators: List of indicators to add. If None, adds all applicable. Options: 'sma', 'ema', 'rsi', 'macd', 'bollinger', 'atr', 'stochastic', 'obv', 'vwap', 'adx', 'supertrend', 'hhv', 'llv', 'mom', 'roc', 'wma', 'dema', 'tema' **kwargs: Additional arguments for specific indicators: - sma_period: SMA period (default 20) - ema_period: EMA period (default 12) - rsi_period: RSI period (default 14) - bb_period: Bollinger Bands period (default 20) - atr_period: ATR period (default 14) - adx_period: ADX period (default 14) - supertrend_period: Supertrend ATR period (default 10) - supertrend_multiplier: Supertrend ATR multiplier (default 3.0) - hhv_period: HHV period (default 14) - llv_period: LLV period (default 14) - mom_period: MOM period (default 10) - roc_period: ROC period (default 10) - wma_period: WMA period (default 20) - dema_period: DEMA period (default 20) - tema_period: TEMA period (default 20)

Returns: DataFrame with indicator columns added

def calculate_sma( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series: View Source

    62def calculate_sma(
    63    df: pd.DataFrame, period: int = 20, column: str = "Close"
    64) -> pd.Series:
    65    """Calculate Simple Moving Average (SMA).
    66
    67    Args:
    68        df: DataFrame with price data
    69        period: Number of periods for moving average
    70        column: Column name to use for calculation
    71
    72    Returns:
    73        Series with SMA values
    74    """
    75    col = _get_price_column(df, column)
    76    if col not in df.columns:
    77        return pd.Series(np.nan, index=df.index, name=f"SMA_{period}")
    78    return df[col].rolling(window=period, min_periods=1).mean()

Calculate Simple Moving Average (SMA).

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

Returns: Series with SMA values

def calculate_ema( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series: View Source

    81def calculate_ema(
    82    df: pd.DataFrame, period: int = 20, column: str = "Close"
    83) -> pd.Series:
    84    """Calculate Exponential Moving Average (EMA).
    85
    86    Args:
    87        df: DataFrame with price data
    88        period: Number of periods for moving average
    89        column: Column name to use for calculation
    90
    91    Returns:
    92        Series with EMA values
    93    """
    94    col = _get_price_column(df, column)
    95    if col not in df.columns:
    96        return pd.Series(np.nan, index=df.index, name=f"EMA_{period}")
    97    return df[col].ewm(span=period, adjust=False).mean()

Calculate Exponential Moving Average (EMA).

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

Returns: Series with EMA values

def calculate_rsi( df: pandas.core.frame.DataFrame, period: int = 14, column: str = 'Close') -> pandas.core.series.Series: View Source

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

Calculate Relative Strength Index (RSI).

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

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

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

Returns: Series with RSI values (0-100)

def calculate_macd( df: pandas.core.frame.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9, column: str = 'Close') -> pandas.core.frame.DataFrame: View Source

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

Calculate Moving Average Convergence Divergence (MACD).

MACD shows the relationship between two moving averages of prices.

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

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

Returns: DataFrame with columns: MACD, Signal, Histogram

def calculate_bollinger_bands( df: pandas.core.frame.DataFrame, period: int = 20, std_dev: float = 2.0, column: str = 'Close') -> pandas.core.frame.DataFrame: View Source

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

Calculate Bollinger Bands.

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

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

Returns: DataFrame with columns: Upper, Middle, Lower

def calculate_atr( df: pandas.core.frame.DataFrame, period: int = 14) -> pandas.core.series.Series: View Source

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

Calculate Average True Range (ATR).

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

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

Returns: Series with ATR values

def calculate_stochastic( df: pandas.core.frame.DataFrame, k_period: int = 14, d_period: int = 3) -> pandas.core.frame.DataFrame: View Source

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

Calculate Stochastic Oscillator (%K and %D).

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

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

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

Returns: DataFrame with columns: Stoch_K, Stoch_D

def calculate_obv(df: pandas.core.frame.DataFrame) -> pandas.core.series.Series: View Source

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

Calculate On-Balance Volume (OBV).

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

Args: df: DataFrame with Close and Volume columns

Returns: Series with OBV values

def calculate_vwap(df: pandas.core.frame.DataFrame) -> pandas.core.series.Series: View Source

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

Calculate Volume Weighted Average Price (VWAP).

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

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

Returns: Series with VWAP values

def calculate_adx( df: pandas.core.frame.DataFrame, period: int = 14) -> pandas.core.series.Series: View Source

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

Calculate Average Directional Index (ADX).

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

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

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

Returns: Series with ADX values

def calculate_supertrend( df: pandas.core.frame.DataFrame, atr_period: int = 10, multiplier: float = 3.0) -> pandas.core.frame.DataFrame: View Source

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

Calculate Supertrend indicator.

Supertrend is a trend-following indicator based on ATR.

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

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

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

def calculate_tilson_t3( df: pandas.core.frame.DataFrame, period: int = 5, vfactor: float = 0.7, column: str = 'Close') -> pandas.core.series.Series: View Source

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

Calculate Tilson T3 Moving Average.

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

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

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

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

Returns: Series with T3 values

Examples:

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

> > > # More responsive (less smooth)

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

> > > # More smooth (more lag)

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

def calculate_hhv( df: pandas.core.frame.DataFrame, period: int = 14, column: str = 'High') -> pandas.core.series.Series: View Source

    567def calculate_hhv(
    568    df: pd.DataFrame, period: int = 14, column: str = "High"
    569) -> pd.Series:
    570    """Calculate Highest High Value (HHV) - MetaStock indicator.
    571
    572    Returns the highest value of a column over a rolling window.
    573
    574    Args:
    575        df: DataFrame with price data
    576        period: Lookback period (default 14)
    577        column: Column to use (default "High")
    578
    579    Returns:
    580        Series with HHV values
    581    """
    582    col = _get_price_column(df, column)
    583    if col not in df.columns:
    584        return pd.Series(np.nan, index=df.index, name=f"HHV_{period}")
    585    return df[col].rolling(window=period, min_periods=1).max()

Calculate Highest High Value (HHV) - MetaStock indicator.

Returns the highest value of a column over a rolling window.

Args: df: DataFrame with price data period: Lookback period (default 14) column: Column to use (default "High")

Returns: Series with HHV values

def calculate_llv( df: pandas.core.frame.DataFrame, period: int = 14, column: str = 'Low') -> pandas.core.series.Series: View Source

    588def calculate_llv(
    589    df: pd.DataFrame, period: int = 14, column: str = "Low"
    590) -> pd.Series:
    591    """Calculate Lowest Low Value (LLV) - MetaStock indicator.
    592
    593    Returns the lowest value of a column over a rolling window.
    594
    595    Args:
    596        df: DataFrame with price data
    597        period: Lookback period (default 14)
    598        column: Column to use (default "Low")
    599
    600    Returns:
    601        Series with LLV values
    602    """
    603    col = _get_price_column(df, column)
    604    if col not in df.columns:
    605        return pd.Series(np.nan, index=df.index, name=f"LLV_{period}")
    606    return df[col].rolling(window=period, min_periods=1).min()

Calculate Lowest Low Value (LLV) - MetaStock indicator.

Returns the lowest value of a column over a rolling window.

Args: df: DataFrame with price data period: Lookback period (default 14) column: Column to use (default "Low")

Returns: Series with LLV values

def calculate_mom( df: pandas.core.frame.DataFrame, period: int = 10, column: str = 'Close') -> pandas.core.series.Series: View Source

    609def calculate_mom(
    610    df: pd.DataFrame, period: int = 10, column: str = "Close"
    611) -> pd.Series:
    612    """Calculate Momentum (MOM) - MetaStock indicator.
    613
    614    MOM = Close - Close[N periods ago]
    615
    616    Args:
    617        df: DataFrame with price data
    618        period: Lookback period (default 10)
    619        column: Column to use (default "Close")
    620
    621    Returns:
    622        Series with Momentum values
    623    """
    624    col = _get_price_column(df, column)
    625    if col not in df.columns:
    626        return pd.Series(np.nan, index=df.index, name=f"MOM_{period}")
    627    return df[col] - df[col].shift(period)

Calculate Momentum (MOM) - MetaStock indicator.

MOM = Close - Close[N periods ago]

Args: df: DataFrame with price data period: Lookback period (default 10) column: Column to use (default "Close")

Returns: Series with Momentum values

def calculate_roc( df: pandas.core.frame.DataFrame, period: int = 10, column: str = 'Close') -> pandas.core.series.Series: View Source

    630def calculate_roc(
    631    df: pd.DataFrame, period: int = 10, column: str = "Close"
    632) -> pd.Series:
    633    """Calculate Rate of Change (ROC) - MetaStock indicator.
    634
    635    ROC = ((Close - Close[N]) / Close[N]) * 100
    636
    637    Args:
    638        df: DataFrame with price data
    639        period: Lookback period (default 10)
    640        column: Column to use (default "Close")
    641
    642    Returns:
    643        Series with ROC values (percentage)
    644    """
    645    col = _get_price_column(df, column)
    646    if col not in df.columns:
    647        return pd.Series(np.nan, index=df.index, name=f"ROC_{period}")
    648    shifted = df[col].shift(period)
    649    return ((df[col] - shifted) / shifted) * 100

Calculate Rate of Change (ROC) - MetaStock indicator.

ROC = ((Close - Close[N]) / Close[N]) * 100

Args: df: DataFrame with price data period: Lookback period (default 10) column: Column to use (default "Close")

Returns: Series with ROC values (percentage)

def calculate_wma( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series: View Source

    652def calculate_wma(
    653    df: pd.DataFrame, period: int = 20, column: str = "Close"
    654) -> pd.Series:
    655    """Calculate Weighted Moving Average (WMA) - MetaStock indicator.
    656
    657    WMA assigns linearly increasing weights to recent data.
    658    Weight for period i = i (most recent gets highest weight).
    659
    660    Args:
    661        df: DataFrame with price data
    662        period: Number of periods (default 20)
    663        column: Column to use (default "Close")
    664
    665    Returns:
    666        Series with WMA values
    667    """
    668    col = _get_price_column(df, column)
    669    if col not in df.columns:
    670        return pd.Series(np.nan, index=df.index, name=f"WMA_{period}")
    671    weights = np.arange(1, period + 1, dtype=float)
    672    return df[col].rolling(window=period, min_periods=period).apply(
    673        lambda x: np.dot(x, weights) / weights.sum(), raw=True
    674    )

Calculate Weighted Moving Average (WMA) - MetaStock indicator.

WMA assigns linearly increasing weights to recent data. Weight for period i = i (most recent gets highest weight).

Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")

Returns: Series with WMA values

def calculate_dema( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series: View Source

    677def calculate_dema(
    678    df: pd.DataFrame, period: int = 20, column: str = "Close"
    679) -> pd.Series:
    680    """Calculate Double Exponential Moving Average (DEMA) - MetaStock indicator.
    681
    682    DEMA = 2 * EMA(Close, N) - EMA(EMA(Close, N), N)
    683
    684    Args:
    685        df: DataFrame with price data
    686        period: Number of periods (default 20)
    687        column: Column to use (default "Close")
    688
    689    Returns:
    690        Series with DEMA values
    691    """
    692    col = _get_price_column(df, column)
    693    if col not in df.columns:
    694        return pd.Series(np.nan, index=df.index, name=f"DEMA_{period}")
    695    ema1 = df[col].ewm(span=period, adjust=False).mean()
    696    ema2 = ema1.ewm(span=period, adjust=False).mean()
    697    return 2 * ema1 - ema2

Calculate Double Exponential Moving Average (DEMA) - MetaStock indicator.

DEMA = 2 * EMA(Close, N) - EMA(EMA(Close, N), N)

Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")

Returns: Series with DEMA values

def calculate_tema( df: pandas.core.frame.DataFrame, period: int = 20, column: str = 'Close') -> pandas.core.series.Series: View Source

    700def calculate_tema(
    701    df: pd.DataFrame, period: int = 20, column: str = "Close"
    702) -> pd.Series:
    703    """Calculate Triple Exponential Moving Average (TEMA) - MetaStock indicator.
    704
    705    TEMA = 3*EMA - 3*EMA(EMA) + EMA(EMA(EMA))
    706
    707    Args:
    708        df: DataFrame with price data
    709        period: Number of periods (default 20)
    710        column: Column to use (default "Close")
    711
    712    Returns:
    713        Series with TEMA values
    714    """
    715    col = _get_price_column(df, column)
    716    if col not in df.columns:
    717        return pd.Series(np.nan, index=df.index, name=f"TEMA_{period}")
    718    ema1 = df[col].ewm(span=period, adjust=False).mean()
    719    ema2 = ema1.ewm(span=period, adjust=False).mean()
    720    ema3 = ema2.ewm(span=period, adjust=False).mean()
    721    return 3 * ema1 - 3 * ema2 + ema3

Calculate Triple Exponential Moving Average (TEMA) - MetaStock indicator.

TEMA = 3EMA - 3EMA(EMA) + EMA(EMA(EMA))

Args: df: DataFrame with price data period: Number of periods (default 20) column: Column to use (default "Close")

Returns: Series with TEMA values

def calculate_heikin_ashi(df: pandas.core.frame.DataFrame) -> pandas.core.frame.DataFrame: View Source

     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: View Source

    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): View Source

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

Base exception for all borsapy errors.

class TickerNotFoundError(borsapy.BorsapyError): View Source

    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) View Source

    14    def __init__(self, symbol: str):
    15        self.symbol = symbol
    16        super().__init__(f"Ticker not found: {symbol}")

symbol
class DataNotAvailableError(borsapy.BorsapyError): View Source

    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') View Source

    22    def __init__(self, message: str = "Data not available"):
    23        super().__init__(message)

class APIError(borsapy.BorsapyError): View Source

    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) View Source

    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): View Source

    36class AuthenticationError(BorsapyError):
    37    """Raised when authentication fails."""
    38
    39    pass

Raised when authentication fails.

class RateLimitError(borsapy.BorsapyError): View Source

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

Raised when rate limit is exceeded.

class InvalidPeriodError(borsapy.BorsapyError): View Source

    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) View Source

    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): View Source

    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) View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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: View Source

    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) View Source

    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: View Source

    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: View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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 View Source

    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]: View Source

    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: View Source

    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: View Source

     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 View Source

    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 View Source

    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 View Source

     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 View Source

    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]: View Source

    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: View Source

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

def withholding_tax_rate( fund_code: str, purchase_date: datetime.date | str | None = None, holding_days: int | None = None) -> float | None: View Source

    248def withholding_tax_rate(
    249    fund_code: str,
    250    purchase_date: date | str | None = None,
    251    holding_days: int | None = None,
    252) -> float | None:
    253    """Get the withholding tax rate for a specific fund.
    254
    255    Convenience wrapper that fetches fund info from TEFAS, classifies the
    256    fund's tax category, and returns the applicable rate.
    257
    258    Args:
    259        fund_code: TEFAS fund code (e.g., "AAK", "TTE").
    260        purchase_date: Date of fund purchase. Defaults to today.
    261        holding_days: Number of days held (relevant for GSYF/GYF funds).
    262
    263    Returns:
    264        Tax rate as a decimal (e.g., 0.15 for 15%), or None if fund
    265        category cannot be determined.
    266
    267    Examples:
    268        >>> import borsapy as bp
    269        >>> bp.withholding_tax_rate("AAK", "2025-06-01")
    270        0.15
    271        >>> bp.withholding_tax_rate("AAK", "2025-08-01")
    272        0.175
    273    """
    274    from borsapy.fund import Fund
    275
    276    if purchase_date is None:
    277        purchase_date = date.today()
    278
    279    fund = Fund(fund_code)
    280    info = fund.info
    281    category = info.get("category", "") or ""
    282    fund_name = info.get("name", "") or ""
    283
    284    tax_cat = classify_fund_tax_category(category, fund_name)
    285    if tax_cat is None:
    286        return None
    287
    288    return get_withholding_tax_rate(tax_cat, purchase_date, holding_days)

Get the withholding tax rate for a specific fund.

Convenience wrapper that fetches fund info from TEFAS, classifies the fund's tax category, and returns the applicable rate.

Args: fund_code: TEFAS fund code (e.g., "AAK", "TTE"). purchase_date: Date of fund purchase. Defaults to today. holding_days: Number of days held (relevant for GSYF/GYF funds).

Returns: Tax rate as a decimal (e.g., 0.15 for 15%), or None if fund category cannot be determined.

Examples:

> > > import borsapy as bp bp.withholding_tax_rate("AAK", "2025-06-01") 0.15 bp.withholding_tax_rate("AAK", "2025-08-01") 0.175

def withholding_tax_table() -> pandas.core.frame.DataFrame: View Source

    291def withholding_tax_table() -> pd.DataFrame:
    292    """Return the full withholding tax reference table.
    293
    294    Returns:
    295        DataFrame with columns: tax_category, description, and one column
    296        per date period showing the tax rate as a percentage.
    297
    298    Examples:
    299        >>> import borsapy as bp
    300        >>> bp.withholding_tax_table()
    301           tax_category                  description  <23.12.2020  ...  >=09.07.2025
    302        0  degisken_karma_doviz  Degisken, karma, ...        10.0  ...          17.5
    303        1  pay_senedi_yogun      Pay senedi yogun fon         0.0  ...           0.0
    304        ...
    305    """
    306    period_labels = [
    307        "<23.12.2020",
    308        "23.12.2020-30.04.2024",
    309        "01.05.2024-31.10.2024",
    310        "01.11.2024-31.01.2025",
    311        "01.02.2025-08.07.2025",
    312        ">=09.07.2025",
    313    ]
    314
    315    rows = []
    316    for cat in [
    317        TAX_CAT_VARIABLE,
    318        TAX_CAT_EQUITY_HEAVY,
    319        TAX_CAT_OTHER,
    320        TAX_CAT_GSYF_GYF_LONG,
    321        TAX_CAT_GSYF_GYF_SHORT,
    322    ]:
    323        row = {
    324            "tax_category": cat,
    325            "description": TAX_CAT_DESCRIPTIONS[cat],
    326        }
    327        for label, rate in zip(period_labels, TAX_RATES[cat], strict=True):
    328            row[label] = rate
    329        rows.append(row)
    330
    331    return pd.DataFrame(rows)

Return the full withholding tax reference table.

Returns: DataFrame with columns: tax_category, description, and one column per date period showing the tax rate as a percentage.

Examples:

> > > import borsapy as bp bp.withholding_tax_table() tax_category description <23.12.2020 ... >=09.07.2025 0 degisken_karma_doviz Degisken, karma, ... 10.0 ... 17.5 1 pay_senedi_yogun Pay senedi yogun fon 0.0 ... 0.0 ...

def set_twitter_auth( auth_token: str | None = None, ct0: str | None = None, cookies: dict | None = None, cookies_file: str | None = None) -> None: View Source

    20def set_twitter_auth(
    21    auth_token: str | None = None,
    22    ct0: str | None = None,
    23    cookies: dict | None = None,
    24    cookies_file: str | None = None,
    25) -> None:
    26    """
    27    Set Twitter/X authentication credentials for tweet search.
    28
    29    Twitter requires cookie-based authentication. You can get these values
    30    from your browser's developer tools after logging into twitter.com/x.com.
    31
    32    Args:
    33        auth_token: The auth_token cookie value from Twitter/X.
    34        ct0: The ct0 cookie value from Twitter/X.
    35        cookies: Dict with 'auth_token' and 'ct0' keys.
    36        cookies_file: Path to a cookies JSON file (Scweet format).
    37
    38    Examples:
    39        >>> import borsapy as bp
    40        >>> # Method 1: Direct cookie values
    41        >>> bp.set_twitter_auth(auth_token="abc123...", ct0="xyz789...")
    42        >>> # Method 2: Dict
    43        >>> bp.set_twitter_auth(cookies={"auth_token": "abc123", "ct0": "xyz789"})
    44        >>> # Method 3: Cookies file
    45        >>> bp.set_twitter_auth(cookies_file="cookies.json")
    46    """
    47    global _twitter_credentials
    48
    49    if cookies_file:
    50        _twitter_credentials = {"cookies_file": cookies_file}
    51    elif cookies:
    52        if "auth_token" not in cookies:
    53            raise ValueError("cookies dict must contain 'auth_token' key")
    54        _twitter_credentials = {"cookies": cookies}
    55    elif auth_token:
    56        _twitter_credentials = {"cookies": {"auth_token": auth_token, "ct0": ct0 or ""}}
    57    else:
    58        raise ValueError(
    59            "Provide auth_token/ct0, cookies dict, or cookies_file. "
    60            "Get auth_token and ct0 from browser DevTools > Application > Cookies > x.com"
    61        )

Set Twitter/X authentication credentials for tweet search.

Twitter requires cookie-based authentication. You can get these values from your browser's developer tools after logging into twitter.com/x.com.

Args: auth_token: The auth_token cookie value from Twitter/X. ct0: The ct0 cookie value from Twitter/X. cookies: Dict with 'auth_token' and 'ct0' keys. cookies_file: Path to a cookies JSON file (Scweet format).

Examples:

> > > import borsapy as bp

> > > # Method 1: Direct cookie values

> > > bp.set_twitter_auth(auth_token="abc123...", ct0="xyz789...")

> > > # Method 2: Dict

> > > bp.set_twitter_auth(cookies={"auth_token": "abc123", "ct0": "xyz789"})

> > > # Method 3: Cookies file

> > > bp.set_twitter_auth(cookies_file="cookies.json")

def get_twitter_auth() -> dict | None: View Source

    70def get_twitter_auth() -> dict | None:
    71    """Get current Twitter/X authentication credentials."""
    72    return _twitter_credentials

Get current Twitter/X authentication credentials.

def clear_twitter_auth() -> None: View Source

    64def clear_twitter_auth() -> None:
    65    """Clear Twitter/X authentication credentials."""
    66    global _twitter_credentials
    67    _twitter_credentials = None

Clear Twitter/X authentication credentials.

def search_tweets( query: str, period: str | None = '7d', since: str | None = None, until: str | None = None, lang: str | None = None, limit: int = 100) -> pandas.core.frame.DataFrame: View Source

    150def search_tweets(
    151    query: str,
    152    period: str | None = "7d",
    153    since: str | None = None,
    154    until: str | None = None,
    155    lang: str | None = None,
    156    limit: int = 100,
    157) -> pd.DataFrame:
    158    """
    159    Search Twitter/X for tweets matching a query.
    160
    161    Requires authentication: call bp.set_twitter_auth() first.
    162    Requires optional dependency: pip install borsapy[twitter]
    163
    164    Args:
    165        query: Search query (e.g., "$THYAO", "dolar kur", "#Bitcoin").
    166        period: Time period ("1d", "7d", "1mo"). Ignored if since/until set.
    167        since: Start date (YYYY-MM-DD). Overrides period.
    168        until: End date (YYYY-MM-DD). Overrides period.
    169        lang: Language filter (e.g., "tr", "en").
    170        limit: Maximum number of tweets (default 100).
    171
    172    Returns:
    173        DataFrame with columns: tweet_id, created_at, text, author_handle,
    174        author_name, likes, retweets, replies, views, quotes, bookmarks,
    175        author_followers, author_verified, lang, url.
    176
    177    Examples:
    178        >>> import borsapy as bp
    179        >>> bp.set_twitter_auth(auth_token="...", ct0="...")
    180        >>> df = bp.search_tweets("$THYAO", period="7d")
    181        >>> df = bp.search_tweets("dolar kur", period="1d", lang="tr", limit=50)
    182    """
    183    provider = get_twitter_provider()
    184    return provider.search_tweets(
    185        query=query,
    186        period=period,
    187        since=since,
    188        until=until,
    189        lang=lang,
    190        limit=limit,
    191    )

Search Twitter/X for tweets matching a query.

Requires authentication: call bp.set_twitter_auth() first. Requires optional dependency: pip install borsapy[twitter]

Args: query: Search query (e.g., "$THYAO", "dolar kur", "#Bitcoin"). period: Time period ("1d", "7d", "1mo"). Ignored if since/until set. since: Start date (YYYY-MM-DD). Overrides period. until: End date (YYYY-MM-DD). Overrides period. lang: Language filter (e.g., "tr", "en"). limit: Maximum number of tweets (default 100).

Returns: DataFrame with columns: tweet_id, created_at, text, author_handle, author_name, likes, retweets, replies, views, quotes, bookmarks, author_followers, author_verified, lang, url.

Examples:

> > > import borsapy as bp bp.set_twitter_auth(auth_token="...", ct0="...") df = bp.search_tweets("$THYAO", period="7d") df = bp.search_tweets("dolar kur", period="1d", lang="tr", limit=50)