borsapy 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
borsapy/__init__.py ADDED
@@ -0,0 +1,134 @@
1
+ """
2
+ borsapy - Turkish Financial Markets Data Library
3
+
4
+ A yfinance-like API for BIST stocks, forex, crypto, funds, and economic data.
5
+
6
+ Examples:
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
+
63
+ from borsapy.bond import Bond, bonds, risk_free_rate
64
+ from borsapy.calendar import EconomicCalendar, economic_calendar
65
+ from borsapy.crypto import Crypto, crypto_pairs
66
+ from borsapy.exceptions import (
67
+ APIError,
68
+ AuthenticationError,
69
+ BorsapyError,
70
+ DataNotAvailableError,
71
+ InvalidIntervalError,
72
+ InvalidPeriodError,
73
+ RateLimitError,
74
+ TickerNotFoundError,
75
+ )
76
+ from borsapy.fund import Fund, compare_funds, screen_funds, search_funds
77
+ from borsapy.fx import FX, banks, metal_institutions
78
+ from borsapy.index import Index, all_indices, index, indices
79
+ from borsapy.inflation import Inflation
80
+ from borsapy.market import companies, search_companies
81
+ from borsapy.multi import Tickers, download
82
+ from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices
83
+ from borsapy.ticker import Ticker
84
+ from borsapy.viop import VIOP
85
+
86
+ __version__ = "0.4.0"
87
+ __author__ = "Said Surucu"
88
+
89
+ __all__ = [
90
+ # Main classes
91
+ "Ticker",
92
+ "Tickers",
93
+ "FX",
94
+ "Crypto",
95
+ "Fund",
96
+ "Index",
97
+ "Inflation",
98
+ "VIOP",
99
+ "Bond",
100
+ "EconomicCalendar",
101
+ "Screener",
102
+ # Market functions
103
+ "companies",
104
+ "search_companies",
105
+ "banks",
106
+ "metal_institutions",
107
+ "crypto_pairs",
108
+ "search_funds",
109
+ "screen_funds",
110
+ "compare_funds",
111
+ "download",
112
+ "index",
113
+ "indices",
114
+ "all_indices",
115
+ # Bond functions
116
+ "bonds",
117
+ "risk_free_rate",
118
+ # Calendar functions
119
+ "economic_calendar",
120
+ # Screener functions
121
+ "screen_stocks",
122
+ "screener_criteria",
123
+ "sectors",
124
+ "stock_indices",
125
+ # Exceptions
126
+ "BorsapyError",
127
+ "TickerNotFoundError",
128
+ "DataNotAvailableError",
129
+ "APIError",
130
+ "AuthenticationError",
131
+ "RateLimitError",
132
+ "InvalidPeriodError",
133
+ "InvalidIntervalError",
134
+ ]
@@ -0,0 +1 @@
1
+ """Data models for borsapy."""
@@ -0,0 +1,5 @@
1
+ """Data providers for borsapy."""
2
+
3
+ from borsapy._providers.base import BaseProvider
4
+
5
+ __all__ = ["BaseProvider"]
@@ -0,0 +1,94 @@
1
+ """Base provider class for all data providers."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from borsapy.cache import Cache, get_cache
8
+
9
+
10
+ class BaseProvider:
11
+ """Base class for all data providers."""
12
+
13
+ DEFAULT_HEADERS = {
14
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
15
+ "Accept": "application/json, text/plain, */*",
16
+ "Accept-Language": "tr-TR,tr;q=0.9,en-US;q=0.8,en;q=0.7",
17
+ }
18
+
19
+ def __init__(
20
+ self,
21
+ timeout: float = 30.0,
22
+ cache: Cache | None = None,
23
+ ):
24
+ """
25
+ Initialize the provider.
26
+
27
+ Args:
28
+ timeout: HTTP request timeout in seconds.
29
+ cache: Cache instance to use. If None, uses global cache.
30
+ """
31
+ self._client = httpx.Client(timeout=timeout, headers=self.DEFAULT_HEADERS)
32
+ self._cache = cache or get_cache()
33
+
34
+ def close(self) -> None:
35
+ """Close the HTTP client."""
36
+ self._client.close()
37
+
38
+ def __enter__(self):
39
+ return self
40
+
41
+ def __exit__(self, exc_type, exc_val, exc_tb):
42
+ self.close()
43
+
44
+ def _get(
45
+ self,
46
+ url: str,
47
+ params: dict[str, Any] | None = None,
48
+ headers: dict[str, str] | None = None,
49
+ ) -> httpx.Response:
50
+ """
51
+ Make a GET request.
52
+
53
+ Args:
54
+ url: Request URL.
55
+ params: Query parameters.
56
+ headers: Request headers.
57
+
58
+ Returns:
59
+ HTTP response.
60
+ """
61
+ response = self._client.get(url, params=params, headers=headers)
62
+ response.raise_for_status()
63
+ return response
64
+
65
+ def _post(
66
+ self,
67
+ url: str,
68
+ data: dict[str, Any] | None = None,
69
+ json: dict[str, Any] | None = None,
70
+ headers: dict[str, str] | None = None,
71
+ ) -> httpx.Response:
72
+ """
73
+ Make a POST request.
74
+
75
+ Args:
76
+ url: Request URL.
77
+ data: Form data.
78
+ json: JSON data.
79
+ headers: Request headers.
80
+
81
+ Returns:
82
+ HTTP response.
83
+ """
84
+ response = self._client.post(url, data=data, json=json, headers=headers)
85
+ response.raise_for_status()
86
+ return response
87
+
88
+ def _cache_get(self, key: str) -> Any | None:
89
+ """Get a value from cache."""
90
+ return self._cache.get(key)
91
+
92
+ def _cache_set(self, key: str, value: Any, ttl: int) -> None:
93
+ """Set a value in cache."""
94
+ self._cache.set(key, value, ttl)
@@ -0,0 +1,150 @@
1
+ """BIST index constituent provider - downloads index components from BIST CSV."""
2
+
3
+ from io import StringIO
4
+ from typing import Any
5
+
6
+ import pandas as pd
7
+
8
+ from borsapy._providers.base import BaseProvider
9
+ from borsapy.cache import TTL
10
+
11
+ # BIST index components CSV URL
12
+ INDEX_COMPONENTS_URL = "https://www.borsaistanbul.com/datum/hisse_endeks_ds.csv"
13
+
14
+ # Singleton instance
15
+ _provider: "BistIndexProvider | None" = None
16
+
17
+
18
+ def get_bist_index_provider() -> "BistIndexProvider":
19
+ """Get or create the singleton BistIndexProvider instance."""
20
+ global _provider
21
+ if _provider is None:
22
+ _provider = BistIndexProvider()
23
+ return _provider
24
+
25
+
26
+ class BistIndexProvider(BaseProvider):
27
+ """Provider for BIST index constituents from official CSV file."""
28
+
29
+ def __init__(self):
30
+ super().__init__(timeout=30.0)
31
+ self._df_cache: pd.DataFrame | None = None
32
+
33
+ def _download_components(self) -> pd.DataFrame | None:
34
+ """Download and cache the components CSV."""
35
+ if self._df_cache is not None:
36
+ return self._df_cache
37
+
38
+ # Check memory cache first
39
+ cache_key = "bist:index:components:all"
40
+ cached = self._cache_get(cache_key)
41
+ if cached is not None:
42
+ self._df_cache = cached
43
+ return cached
44
+
45
+ try:
46
+ response = self._get(INDEX_COMPONENTS_URL)
47
+ df = pd.read_csv(StringIO(response.text), sep=";")
48
+ # Skip header row (has English column names)
49
+ df = df.iloc[1:]
50
+ # Clean up symbol codes (remove .E suffix)
51
+ df["symbol"] = df["BILESEN KODU"].str.replace(r"\.E$", "", regex=True)
52
+ df["name"] = df["BULTEN_ADI"]
53
+ df["index_code"] = df["ENDEKS KODU"]
54
+ df["index_name"] = df["ENDEKS ADI"]
55
+
56
+ self._df_cache = df
57
+ self._cache_set(cache_key, df, TTL.COMPANY_LIST)
58
+ return df
59
+ except Exception:
60
+ return None
61
+
62
+ def get_components(self, symbol: str) -> list[dict[str, Any]]:
63
+ """
64
+ Get constituent stocks for an index.
65
+
66
+ Args:
67
+ symbol: Index symbol (e.g., "XU100", "XU030", "XKTUM").
68
+
69
+ Returns:
70
+ List of component dicts with 'symbol' and 'name' keys.
71
+ Empty list if index not found or fetch fails.
72
+
73
+ Examples:
74
+ >>> provider = get_bist_index_provider()
75
+ >>> provider.get_components("XU030")
76
+ [{'symbol': 'AKBNK', 'name': 'AKBANK'}, ...]
77
+ """
78
+ symbol = symbol.upper()
79
+
80
+ df = self._download_components()
81
+ if df is None:
82
+ return []
83
+
84
+ # Filter by index code
85
+ mask = df["index_code"] == symbol
86
+ components = df[mask][["symbol", "name"]].to_dict("records")
87
+
88
+ return components
89
+
90
+ def get_available_indices(self) -> list[dict[str, Any]]:
91
+ """
92
+ Get list of all indices with component counts.
93
+
94
+ Returns:
95
+ List of dicts with 'symbol', 'name', and 'count' keys.
96
+ """
97
+ df = self._download_components()
98
+ if df is None:
99
+ return []
100
+
101
+ # Group by index code
102
+ grouped = df.groupby(["index_code", "index_name"]).size().reset_index(name="count")
103
+ indices = [
104
+ {"symbol": row["index_code"], "name": row["index_name"], "count": row["count"]}
105
+ for _, row in grouped.iterrows()
106
+ ]
107
+
108
+ return sorted(indices, key=lambda x: x["symbol"])
109
+
110
+ def is_in_index(self, ticker: str, index_symbol: str) -> bool:
111
+ """
112
+ Check if a stock is in a specific index.
113
+
114
+ Args:
115
+ ticker: Stock symbol (e.g., "THYAO").
116
+ index_symbol: Index symbol (e.g., "XU030").
117
+
118
+ Returns:
119
+ True if stock is in the index.
120
+ """
121
+ ticker = ticker.upper()
122
+ index_symbol = index_symbol.upper()
123
+
124
+ df = self._download_components()
125
+ if df is None:
126
+ return False
127
+
128
+ mask = (df["symbol"] == ticker) & (df["index_code"] == index_symbol)
129
+ return mask.any()
130
+
131
+ def get_indices_for_ticker(self, ticker: str) -> list[str]:
132
+ """
133
+ Get all indices that contain a specific stock.
134
+
135
+ Args:
136
+ ticker: Stock symbol (e.g., "THYAO").
137
+
138
+ Returns:
139
+ List of index symbols that contain this stock.
140
+ """
141
+ ticker = ticker.upper()
142
+
143
+ df = self._download_components()
144
+ if df is None:
145
+ return []
146
+
147
+ mask = df["symbol"] == ticker
148
+ indices = df[mask]["index_code"].unique().tolist()
149
+
150
+ return sorted(indices)
@@ -0,0 +1,230 @@
1
+ """BtcTurk provider for cryptocurrency data."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from typing import Any
5
+
6
+ import pandas as pd
7
+
8
+ from borsapy._providers.base import BaseProvider
9
+ from borsapy.cache import TTL
10
+ from borsapy.exceptions import APIError, DataNotAvailableError
11
+
12
+
13
+ class BtcTurkProvider(BaseProvider):
14
+ """
15
+ Provider for cryptocurrency data from BtcTurk.
16
+
17
+ Provides:
18
+ - Real-time ticker data for crypto pairs
19
+ - Historical OHLC data
20
+ """
21
+
22
+ BASE_URL = "https://api.btcturk.com/api/v2"
23
+ GRAPH_API_URL = "https://graph-api.btcturk.com"
24
+
25
+ # Resolution mapping (minutes)
26
+ RESOLUTION_MAP = {
27
+ "1m": 1,
28
+ "5m": 5,
29
+ "15m": 15,
30
+ "30m": 30,
31
+ "1h": 60,
32
+ "4h": 240,
33
+ "1d": 1440,
34
+ "1wk": 10080,
35
+ }
36
+
37
+ def get_ticker(self, pair: str) -> dict[str, Any]:
38
+ """
39
+ Get current ticker data for a crypto pair.
40
+
41
+ Args:
42
+ pair: Trading pair (e.g., "BTCTRY", "ETHTRY", "BTCUSDT")
43
+
44
+ Returns:
45
+ Dictionary with ticker data.
46
+ """
47
+ pair = pair.upper()
48
+
49
+ cache_key = f"btcturk:ticker:{pair}"
50
+ cached = self._cache_get(cache_key)
51
+ if cached is not None:
52
+ return cached
53
+
54
+ try:
55
+ url = f"{self.BASE_URL}/ticker"
56
+ params = {"pairSymbol": pair}
57
+
58
+ response = self._client.get(url, params=params)
59
+ response.raise_for_status()
60
+ data = response.json()
61
+
62
+ if not data.get("success", False):
63
+ raise APIError(f"API error: {data.get('message', 'Unknown')}")
64
+
65
+ ticker_data = data.get("data", [])
66
+ if not ticker_data:
67
+ raise DataNotAvailableError(f"No data for pair: {pair}")
68
+
69
+ ticker = ticker_data[0] if isinstance(ticker_data, list) else ticker_data
70
+
71
+ result = {
72
+ "symbol": ticker.get("pair"),
73
+ "last": float(ticker.get("last", 0)),
74
+ "open": float(ticker.get("open", 0)),
75
+ "high": float(ticker.get("high", 0)),
76
+ "low": float(ticker.get("low", 0)),
77
+ "bid": float(ticker.get("bid", 0)),
78
+ "ask": float(ticker.get("ask", 0)),
79
+ "volume": float(ticker.get("volume", 0)),
80
+ "change": float(ticker.get("daily", 0)),
81
+ "change_percent": float(ticker.get("dailyPercent", 0)),
82
+ "timestamp": ticker.get("timestamp"),
83
+ }
84
+
85
+ self._cache_set(cache_key, result, TTL.REALTIME_PRICE)
86
+ return result
87
+
88
+ except Exception as e:
89
+ raise APIError(f"Failed to fetch ticker for {pair}: {e}") from e
90
+
91
+ def get_history(
92
+ self,
93
+ pair: str,
94
+ period: str = "1mo",
95
+ interval: str = "1d",
96
+ start: datetime | None = None,
97
+ end: datetime | None = None,
98
+ ) -> pd.DataFrame:
99
+ """
100
+ Get historical OHLC data for a crypto pair.
101
+
102
+ Args:
103
+ pair: Trading pair (e.g., "BTCTRY", "ETHTRY")
104
+ period: Data period (1d, 5d, 1mo, 3mo, 6mo, 1y)
105
+ interval: Data interval (1m, 5m, 15m, 30m, 1h, 4h, 1d, 1wk)
106
+ start: Start date
107
+ end: End date
108
+
109
+ Returns:
110
+ DataFrame with OHLCV data.
111
+ """
112
+ pair = pair.upper()
113
+
114
+ # Calculate time range
115
+ end_dt = end or datetime.now()
116
+ if start:
117
+ start_dt = start
118
+ else:
119
+ days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90, "6mo": 180, "1y": 365}.get(period, 30)
120
+ start_dt = end_dt - timedelta(days=days)
121
+
122
+ from_ts = int(start_dt.timestamp())
123
+ to_ts = int(end_dt.timestamp())
124
+
125
+ cache_key = f"btcturk:history:{pair}:{interval}:{from_ts}:{to_ts}"
126
+ cached = self._cache_get(cache_key)
127
+ if cached is not None:
128
+ return cached
129
+
130
+ try:
131
+ # Get resolution in minutes
132
+ resolution = self.RESOLUTION_MAP.get(interval, 1440)
133
+
134
+ url = f"{self.GRAPH_API_URL}/v1/klines/history"
135
+ params = {
136
+ "symbol": pair,
137
+ "resolution": resolution,
138
+ "from": from_ts,
139
+ "to": to_ts,
140
+ }
141
+
142
+ response = self._client.get(url, params=params)
143
+ response.raise_for_status()
144
+ data = response.json()
145
+
146
+ # Graph API returns TradingView format
147
+ status = data.get("s", "error")
148
+ if status != "ok":
149
+ raise DataNotAvailableError(f"No data available for {pair}")
150
+
151
+ # Parse TradingView format
152
+ timestamps = data.get("t", [])
153
+ opens = data.get("o", [])
154
+ highs = data.get("h", [])
155
+ lows = data.get("l", [])
156
+ closes = data.get("c", [])
157
+ volumes = data.get("v", [])
158
+
159
+ records = []
160
+ for i in range(len(timestamps)):
161
+ records.append(
162
+ {
163
+ "Date": datetime.fromtimestamp(timestamps[i]),
164
+ "Open": float(opens[i]) if i < len(opens) else 0.0,
165
+ "High": float(highs[i]) if i < len(highs) else 0.0,
166
+ "Low": float(lows[i]) if i < len(lows) else 0.0,
167
+ "Close": float(closes[i]) if i < len(closes) else 0.0,
168
+ "Volume": float(volumes[i]) if i < len(volumes) else 0.0,
169
+ }
170
+ )
171
+
172
+ df = pd.DataFrame(records)
173
+ if not df.empty:
174
+ df.set_index("Date", inplace=True)
175
+ df.sort_index(inplace=True)
176
+
177
+ self._cache_set(cache_key, df, TTL.OHLCV_HISTORY)
178
+ return df
179
+
180
+ except Exception as e:
181
+ raise APIError(f"Failed to fetch history for {pair}: {e}") from e
182
+
183
+ def get_pairs(self, quote: str = "TRY") -> list[str]:
184
+ """
185
+ Get list of available trading pairs.
186
+
187
+ Args:
188
+ quote: Quote currency filter (TRY, USDT, BTC)
189
+
190
+ Returns:
191
+ List of trading pair symbols.
192
+ """
193
+ cache_key = f"btcturk:pairs:{quote}"
194
+ cached = self._cache_get(cache_key)
195
+ if cached is not None:
196
+ return cached
197
+
198
+ try:
199
+ url = f"{self.BASE_URL}/ticker"
200
+ response = self._client.get(url)
201
+ response.raise_for_status()
202
+ data = response.json()
203
+
204
+ if not data.get("success", False):
205
+ return []
206
+
207
+ pairs = []
208
+ quote_upper = quote.upper()
209
+ for ticker in data.get("data", []):
210
+ pair = ticker.get("pair", "")
211
+ if pair.endswith(quote_upper):
212
+ pairs.append(pair)
213
+
214
+ self._cache_set(cache_key, pairs, TTL.COMPANY_LIST)
215
+ return pairs
216
+
217
+ except Exception:
218
+ return []
219
+
220
+
221
+ # Singleton
222
+ _provider: BtcTurkProvider | None = None
223
+
224
+
225
+ def get_btcturk_provider() -> BtcTurkProvider:
226
+ """Get singleton provider instance."""
227
+ global _provider
228
+ if _provider is None:
229
+ _provider = BtcTurkProvider()
230
+ return _provider