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.
@@ -0,0 +1,204 @@
1
+ """VİOP provider for derivatives data via İş Yatırım HTML scraping."""
2
+
3
+ import pandas as pd
4
+ from bs4 import BeautifulSoup
5
+
6
+ from borsapy._providers.base import BaseProvider
7
+ from borsapy.cache import TTL
8
+ from borsapy.exceptions import APIError
9
+
10
+ # Singleton instance
11
+ _viop_provider: "ViOpProvider | None" = None
12
+
13
+
14
+ def get_viop_provider() -> "ViOpProvider":
15
+ """Get singleton VİOP provider instance."""
16
+ global _viop_provider
17
+ if _viop_provider is None:
18
+ _viop_provider = ViOpProvider()
19
+ return _viop_provider
20
+
21
+
22
+ class ViOpProvider(BaseProvider):
23
+ """
24
+ Provider for VİOP (Vadeli İşlem ve Opsiyon Piyasası) data.
25
+
26
+ Data source: İş Yatırım VİOP page (HTML scraping)
27
+ Note: Data is delayed by ~15 minutes (Matriks source)
28
+ """
29
+
30
+ URL = "https://www.isyatirim.com.tr/tr-tr/analiz/Sayfalar/viop.aspx"
31
+
32
+ # Table section identifiers (Turkish)
33
+ SECTIONS = {
34
+ "stock_futures": "Pay Vadeli İşlem Ana Pazarı",
35
+ "index_futures": "Endeks Vadeli İşlem Ana Pazarı",
36
+ "currency_futures": "Döviz Vadeli İşlem Ana Pazarı",
37
+ "commodity_futures": "Kıymetli Madenler Vadeli İşlem Ana Pazarı",
38
+ "stock_options": "Pay Opsiyon Ana Pazarı",
39
+ "index_options": "Endeks Opsiyon Ana Pazarı",
40
+ }
41
+
42
+ def _fetch_page(self) -> BeautifulSoup:
43
+ """Fetch and parse VİOP page."""
44
+ cache_key = "viop:page"
45
+ cached = self._cache_get(cache_key)
46
+ if cached is not None:
47
+ return cached
48
+
49
+ try:
50
+ response = self._get(self.URL)
51
+ soup = BeautifulSoup(response.text, "html.parser")
52
+ self._cache_set(cache_key, soup, TTL.VIOP)
53
+ return soup
54
+ except Exception as e:
55
+ raise APIError(f"Failed to fetch VİOP page: {e}") from e
56
+
57
+ def _parse_table(self, soup: BeautifulSoup, section_name: str) -> pd.DataFrame:
58
+ """Parse a VİOP table section."""
59
+ # Find accordion with the section name
60
+ accordion = None
61
+ for a_tag in soup.find_all("a"):
62
+ if a_tag.get_text(strip=True) == section_name:
63
+ accordion = a_tag.find_parent("div", class_="accordion-item")
64
+ break
65
+
66
+ if accordion is None:
67
+ return pd.DataFrame()
68
+
69
+ # Find table within accordion
70
+ table = accordion.find("table")
71
+ if table is None:
72
+ return pd.DataFrame()
73
+
74
+ rows = []
75
+ for tr in table.find_all("tr"):
76
+ tds = tr.find_all("td")
77
+ if len(tds) >= 5:
78
+ # Extract contract code from title attribute
79
+ first_td = tds[0]
80
+ title = first_td.get("title", "")
81
+ contract_code = ""
82
+ if "|" in title:
83
+ contract_code = title.split("|")[0].strip()
84
+
85
+ contract_name = first_td.get_text(strip=True)
86
+ price = self._parse_number(tds[1].get_text(strip=True))
87
+ change = self._parse_number(tds[2].get_text(strip=True))
88
+ volume_tl = self._parse_number(tds[3].get_text(strip=True))
89
+ volume_qty = self._parse_number(tds[4].get_text(strip=True))
90
+
91
+ rows.append({
92
+ "code": contract_code,
93
+ "contract": contract_name,
94
+ "price": price,
95
+ "change": change,
96
+ "volume_tl": volume_tl,
97
+ "volume_qty": volume_qty,
98
+ })
99
+
100
+ if not rows:
101
+ return pd.DataFrame()
102
+
103
+ return pd.DataFrame(rows)
104
+
105
+ def _parse_number(self, text: str) -> float | None:
106
+ """Parse Turkish number format (1.234,56 -> 1234.56)."""
107
+ if not text:
108
+ return None
109
+ try:
110
+ # Remove thousand separator (.) and convert decimal separator (,) to (.)
111
+ cleaned = text.replace(".", "").replace(",", ".")
112
+ return float(cleaned)
113
+ except (ValueError, TypeError):
114
+ return None
115
+
116
+ def get_futures(self, category: str = "all") -> pd.DataFrame:
117
+ """
118
+ Get futures contracts.
119
+
120
+ Args:
121
+ category: Filter by category:
122
+ - "all": All futures
123
+ - "stock": Stock futures (Pay Vadeli)
124
+ - "index": Index futures (Endeks Vadeli)
125
+ - "currency": Currency futures (Döviz Vadeli)
126
+ - "commodity": Commodity futures (Kıymetli Madenler)
127
+
128
+ Returns:
129
+ DataFrame with columns: code, contract, price, change, volume_tl, volume_qty
130
+ """
131
+ soup = self._fetch_page()
132
+
133
+ category_map = {
134
+ "stock": ["stock_futures"],
135
+ "index": ["index_futures"],
136
+ "currency": ["currency_futures"],
137
+ "commodity": ["commodity_futures"],
138
+ "all": ["stock_futures", "index_futures", "currency_futures", "commodity_futures"],
139
+ }
140
+
141
+ sections = category_map.get(category, category_map["all"])
142
+ dfs = []
143
+
144
+ for section_key in sections:
145
+ section_name = self.SECTIONS.get(section_key)
146
+ if section_name:
147
+ df = self._parse_table(soup, section_name)
148
+ if not df.empty:
149
+ df["category"] = section_key.replace("_futures", "")
150
+ dfs.append(df)
151
+
152
+ if not dfs:
153
+ return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"])
154
+
155
+ return pd.concat(dfs, ignore_index=True)
156
+
157
+ def get_options(self, category: str = "all") -> pd.DataFrame:
158
+ """
159
+ Get options contracts.
160
+
161
+ Args:
162
+ category: Filter by category:
163
+ - "all": All options
164
+ - "stock": Stock options (Pay Opsiyon)
165
+ - "index": Index options (Endeks Opsiyon)
166
+
167
+ Returns:
168
+ DataFrame with columns: code, contract, price, change, volume_tl, volume_qty
169
+ """
170
+ soup = self._fetch_page()
171
+
172
+ category_map = {
173
+ "stock": ["stock_options"],
174
+ "index": ["index_options"],
175
+ "all": ["stock_options", "index_options"],
176
+ }
177
+
178
+ sections = category_map.get(category, category_map["all"])
179
+ dfs = []
180
+
181
+ for section_key in sections:
182
+ section_name = self.SECTIONS.get(section_key)
183
+ if section_name:
184
+ df = self._parse_table(soup, section_name)
185
+ if not df.empty:
186
+ df["category"] = section_key.replace("_options", "")
187
+ dfs.append(df)
188
+
189
+ if not dfs:
190
+ return pd.DataFrame(columns=["code", "contract", "price", "change", "volume_tl", "volume_qty", "category"])
191
+
192
+ return pd.concat(dfs, ignore_index=True)
193
+
194
+ def get_all(self) -> dict[str, pd.DataFrame]:
195
+ """
196
+ Get all VİOP data.
197
+
198
+ Returns:
199
+ Dictionary with 'futures' and 'options' DataFrames.
200
+ """
201
+ return {
202
+ "futures": self.get_futures("all"),
203
+ "options": self.get_options("all"),
204
+ }
borsapy/bond.py ADDED
@@ -0,0 +1,162 @@
1
+ """Bond class for Turkish government bond yields - yfinance-like API."""
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+
7
+ from borsapy._providers.dovizcom_tahvil import get_tahvil_provider
8
+
9
+
10
+ class Bond:
11
+ """
12
+ A yfinance-like interface for Turkish government bond data.
13
+
14
+ Data source: doviz.com/tahvil
15
+
16
+ Examples:
17
+ >>> import borsapy as bp
18
+ >>> bond = bp.Bond("10Y")
19
+ >>> bond.yield_rate # Current yield (e.g., 28.03)
20
+ 28.03
21
+ >>> bond.yield_decimal # As decimal (e.g., 0.2803)
22
+ 0.2803
23
+ >>> bond.change_pct # Daily change percentage
24
+ 1.5
25
+
26
+ >>> bp.bonds() # Get all bond yields
27
+ name maturity yield change change_pct
28
+ 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54
29
+ 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31
30
+ 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
31
+ """
32
+
33
+ # Valid maturities
34
+ MATURITIES = ["2Y", "5Y", "10Y"]
35
+
36
+ def __init__(self, maturity: str):
37
+ """
38
+ Initialize a Bond object.
39
+
40
+ Args:
41
+ maturity: Bond maturity (2Y, 5Y, 10Y).
42
+ """
43
+ self._maturity = maturity.upper()
44
+ self._provider = get_tahvil_provider()
45
+ self._data_cache: dict[str, Any] | None = None
46
+
47
+ @property
48
+ def maturity(self) -> str:
49
+ """Return the bond maturity."""
50
+ return self._maturity
51
+
52
+ @property
53
+ def _data(self) -> dict[str, Any]:
54
+ """Get bond data (cached)."""
55
+ if self._data_cache is None:
56
+ self._data_cache = self._provider.get_bond(self._maturity)
57
+ return self._data_cache
58
+
59
+ @property
60
+ def name(self) -> str:
61
+ """Return the bond name."""
62
+ return self._data.get("name", "")
63
+
64
+ @property
65
+ def yield_rate(self) -> float | None:
66
+ """
67
+ Return the current yield as percentage.
68
+
69
+ Returns:
70
+ Yield rate as percentage (e.g., 28.03 for 28.03%).
71
+ """
72
+ return self._data.get("yield")
73
+
74
+ @property
75
+ def yield_decimal(self) -> float | None:
76
+ """
77
+ Return the current yield as decimal.
78
+
79
+ Returns:
80
+ Yield rate as decimal (e.g., 0.2803 for 28.03%).
81
+ Useful for financial calculations.
82
+ """
83
+ return self._data.get("yield_decimal")
84
+
85
+ @property
86
+ def change(self) -> float | None:
87
+ """Return the absolute change in yield."""
88
+ return self._data.get("change")
89
+
90
+ @property
91
+ def change_pct(self) -> float | None:
92
+ """Return the percentage change in yield."""
93
+ return self._data.get("change_pct")
94
+
95
+ @property
96
+ def info(self) -> dict[str, Any]:
97
+ """
98
+ Return all bond information.
99
+
100
+ Returns:
101
+ Dictionary with name, maturity, yield, change, etc.
102
+ """
103
+ return self._data.copy()
104
+
105
+ def __repr__(self) -> str:
106
+ return f"Bond('{self._maturity}')"
107
+
108
+
109
+ def bonds() -> pd.DataFrame:
110
+ """
111
+ Get all Turkish government bond yields.
112
+
113
+ Returns:
114
+ DataFrame with columns: name, maturity, yield, change, change_pct.
115
+
116
+ Examples:
117
+ >>> import borsapy as bp
118
+ >>> bp.bonds()
119
+ name maturity yield change change_pct
120
+ 0 2 Yıllık Tahvil 2Y 26.42 0.40 1.54
121
+ 1 5 Yıllık Tahvil 5Y 27.15 0.35 1.31
122
+ 2 10 Yıllık Tahvil 10Y 28.03 0.42 1.52
123
+ """
124
+ provider = get_tahvil_provider()
125
+ data = provider.get_bond_yields()
126
+
127
+ if not data:
128
+ return pd.DataFrame(columns=["name", "maturity", "yield", "change", "change_pct"])
129
+
130
+ df = pd.DataFrame(data)
131
+
132
+ # Select and rename columns
133
+ columns = {
134
+ "name": "name",
135
+ "maturity": "maturity",
136
+ "yield": "yield",
137
+ "change": "change",
138
+ "change_pct": "change_pct",
139
+ }
140
+
141
+ df = df[[c for c in columns.keys() if c in df.columns]]
142
+ df = df.rename(columns=columns)
143
+
144
+ return df
145
+
146
+
147
+ def risk_free_rate() -> float | None:
148
+ """
149
+ Get the risk-free rate for Turkish market (10Y bond yield).
150
+
151
+ Returns:
152
+ 10-year government bond yield as decimal.
153
+ Useful for CAPM and DCF calculations.
154
+
155
+ Examples:
156
+ >>> import borsapy as bp
157
+ >>> rfr = bp.risk_free_rate()
158
+ >>> rfr
159
+ 0.2803
160
+ """
161
+ provider = get_tahvil_provider()
162
+ return provider.get_10y_yield()
borsapy/cache.py ADDED
@@ -0,0 +1,86 @@
1
+ """TTL-based in-memory cache for borsapy."""
2
+
3
+ import time
4
+ from dataclasses import dataclass, field
5
+ from threading import Lock
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ @dataclass
12
+ class CacheEntry(Generic[T]):
13
+ """A single cache entry with value and expiration time."""
14
+
15
+ value: T
16
+ expires_at: float
17
+
18
+
19
+ @dataclass
20
+ class Cache:
21
+ """Thread-safe TTL-based in-memory cache."""
22
+
23
+ _store: dict[str, CacheEntry] = field(default_factory=dict)
24
+ _lock: Lock = field(default_factory=Lock)
25
+
26
+ def get(self, key: str) -> Any | None:
27
+ """Get a value from cache if it exists and hasn't expired."""
28
+ with self._lock:
29
+ entry = self._store.get(key)
30
+ if entry is None:
31
+ return None
32
+ if time.time() > entry.expires_at:
33
+ del self._store[key]
34
+ return None
35
+ return entry.value
36
+
37
+ def set(self, key: str, value: Any, ttl_seconds: int) -> None:
38
+ """Set a value in cache with TTL in seconds."""
39
+ with self._lock:
40
+ self._store[key] = CacheEntry(value=value, expires_at=time.time() + ttl_seconds)
41
+
42
+ def delete(self, key: str) -> bool:
43
+ """Delete a key from cache. Returns True if key existed."""
44
+ with self._lock:
45
+ if key in self._store:
46
+ del self._store[key]
47
+ return True
48
+ return False
49
+
50
+ def clear(self) -> None:
51
+ """Clear all entries from cache."""
52
+ with self._lock:
53
+ self._store.clear()
54
+
55
+ def cleanup(self) -> int:
56
+ """Remove expired entries. Returns number of entries removed."""
57
+ with self._lock:
58
+ now = time.time()
59
+ expired_keys = [k for k, v in self._store.items() if now > v.expires_at]
60
+ for key in expired_keys:
61
+ del self._store[key]
62
+ return len(expired_keys)
63
+
64
+
65
+ # TTL values in seconds
66
+ class TTL:
67
+ """Standard TTL values for different data types."""
68
+
69
+ REALTIME_PRICE = 60 # 1 minute
70
+ OHLCV_HISTORY = 3600 # 1 hour
71
+ COMPANY_INFO = 3600 # 1 hour
72
+ FINANCIAL_STATEMENTS = 86400 # 24 hours
73
+ FX_RATES = 300 # 5 minutes
74
+ COMPANY_LIST = 86400 # 24 hours
75
+ FUND_DATA = 3600 # 1 hour
76
+ INFLATION_DATA = 86400 # 24 hours
77
+ VIOP = 300 # 5 minutes (delayed data)
78
+
79
+
80
+ # Global cache instance
81
+ _cache = Cache()
82
+
83
+
84
+ def get_cache() -> Cache:
85
+ """Get the global cache instance."""
86
+ return _cache