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,276 @@
1
+ """Doviz.com Economic Calendar provider for borsapy."""
2
+
3
+ import re
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+ from bs4 import BeautifulSoup
8
+
9
+ from borsapy._providers.base import BaseProvider
10
+ from borsapy.cache import TTL
11
+ from borsapy.exceptions import APIError
12
+
13
+
14
+ class DovizcomCalendarProvider(BaseProvider):
15
+ """
16
+ Provider for economic calendar data from doviz.com.
17
+
18
+ API: https://www.doviz.com/calendar/getCalendarEvents
19
+ """
20
+
21
+ BASE_URL = "https://www.doviz.com/calendar/getCalendarEvents"
22
+ BEARER_TOKEN = "d00c1214cbca6a7a1b4728a8cc78cd69ba99e0d2ddb6d0687d2ed34f6a547b48"
23
+
24
+ # Country code mapping
25
+ COUNTRY_MAP = {
26
+ "TR": "Türkiye",
27
+ "US": "ABD",
28
+ "EU": "Euro Bölgesi",
29
+ "DE": "Almanya",
30
+ "GB": "Birleşik Krallık",
31
+ "JP": "Japonya",
32
+ "CN": "Çin",
33
+ "FR": "Fransa",
34
+ "IT": "İtalya",
35
+ "CA": "Kanada",
36
+ "AU": "Avustralya",
37
+ "CH": "İsviçre",
38
+ "KR": "Güney Kore",
39
+ "BR": "Brezilya",
40
+ "IN": "Hindistan",
41
+ "RU": "Rusya",
42
+ }
43
+
44
+ # Turkish month names
45
+ TURKISH_MONTHS = {
46
+ "Ocak": 1,
47
+ "Şubat": 2,
48
+ "Mart": 3,
49
+ "Nisan": 4,
50
+ "Mayıs": 5,
51
+ "Haziran": 6,
52
+ "Temmuz": 7,
53
+ "Ağustos": 8,
54
+ "Eylül": 9,
55
+ "Ekim": 10,
56
+ "Kasım": 11,
57
+ "Aralık": 12,
58
+ }
59
+
60
+ def _get_auth_headers(self) -> dict[str, str]:
61
+ """Get headers with Bearer token for doviz.com API."""
62
+ return {
63
+ **self.DEFAULT_HEADERS,
64
+ "Authorization": f"Bearer {self.BEARER_TOKEN}",
65
+ "Accept": "application/json",
66
+ }
67
+
68
+ def _parse_turkish_date(self, date_str: str) -> datetime | None:
69
+ """Parse Turkish date format like '30 Haziran 2025'."""
70
+ try:
71
+ parts = date_str.strip().split()
72
+ if len(parts) == 3:
73
+ day = int(parts[0])
74
+ month = self.TURKISH_MONTHS.get(parts[1])
75
+ year = int(parts[2])
76
+ if month:
77
+ return datetime(year, month, day)
78
+ except (ValueError, IndexError):
79
+ pass
80
+ return None
81
+
82
+ def _parse_time(self, time_str: str) -> str | None:
83
+ """Parse time string like '10:00'."""
84
+ if not time_str:
85
+ return None
86
+ time_str = time_str.strip()
87
+ if re.match(r"^\d{1,2}:\d{2}$", time_str):
88
+ return time_str
89
+ return None
90
+
91
+ def _extract_period(self, event_name: str) -> str:
92
+ """Extract period from event name like 'Enflasyon (Haziran)'."""
93
+ match = re.search(r"\(([^)]+)\)$", event_name)
94
+ if match:
95
+ return match.group(1)
96
+ return ""
97
+
98
+ def _parse_html(
99
+ self, html_content: str, country_code: str
100
+ ) -> list[dict[str, Any]]:
101
+ """Parse HTML content and extract economic events."""
102
+ soup = BeautifulSoup(html_content, "html.parser")
103
+ events = []
104
+ current_date = None
105
+
106
+ # Find content containers
107
+ content_divs = soup.find_all(
108
+ "div", id=lambda x: x and "calendar-content-" in x
109
+ )
110
+
111
+ for content_div in content_divs:
112
+ # Find date header
113
+ date_header = content_div.find(
114
+ "div", class_="text-center mt-8 mb-8 text-bold"
115
+ )
116
+ if date_header:
117
+ date_text = date_header.get_text(strip=True)
118
+ current_date = self._parse_turkish_date(date_text)
119
+
120
+ # Find event rows
121
+ rows = content_div.find_all("tr")
122
+
123
+ for row in rows:
124
+ cells = row.find_all("td")
125
+ if len(cells) >= 7:
126
+ try:
127
+ time_cell = cells[0]
128
+ importance_cell = cells[2]
129
+ event_cell = cells[3]
130
+ actual_cell = cells[4]
131
+ expected_cell = cells[5]
132
+ previous_cell = cells[6]
133
+
134
+ # Parse data
135
+ event_time = self._parse_time(time_cell.get_text(strip=True))
136
+ event_name = event_cell.get_text(strip=True)
137
+
138
+ # Parse importance
139
+ importance = "low"
140
+ importance_span = importance_cell.find(
141
+ "span", class_=lambda x: x and "importance" in str(x)
142
+ )
143
+ if importance_span:
144
+ classes = importance_span.get("class", [])
145
+ for cls in classes:
146
+ if cls in ["low", "mid", "high"]:
147
+ importance = cls
148
+ break
149
+
150
+ actual = actual_cell.get_text(strip=True) or None
151
+ expected = expected_cell.get_text(strip=True) or None
152
+ previous = previous_cell.get_text(strip=True) or None
153
+
154
+ if event_name and current_date:
155
+ events.append(
156
+ {
157
+ "date": current_date,
158
+ "time": event_time,
159
+ "country_code": country_code,
160
+ "country": self.COUNTRY_MAP.get(
161
+ country_code, country_code
162
+ ),
163
+ "event": event_name,
164
+ "importance": importance,
165
+ "period": self._extract_period(event_name),
166
+ "actual": actual,
167
+ "forecast": expected,
168
+ "previous": previous,
169
+ }
170
+ )
171
+ except Exception:
172
+ continue
173
+
174
+ return events
175
+
176
+ def get_economic_calendar(
177
+ self,
178
+ start: datetime | None = None,
179
+ end: datetime | None = None,
180
+ countries: list[str] | None = None,
181
+ importance: str | None = None,
182
+ ) -> list[dict[str, Any]]:
183
+ """
184
+ Get economic calendar events.
185
+
186
+ Args:
187
+ start: Start date. Defaults to today.
188
+ end: End date. Defaults to start + 7 days.
189
+ countries: List of country codes (TR, US, EU, etc.). Defaults to ['TR', 'US'].
190
+ importance: Filter by importance level ('low', 'mid', 'high').
191
+
192
+ Returns:
193
+ List of economic events with date, time, country, event, importance, etc.
194
+ """
195
+ from datetime import timedelta
196
+
197
+ # Defaults
198
+ if start is None:
199
+ start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
200
+ if end is None:
201
+ end = start + timedelta(days=7)
202
+ if countries is None:
203
+ countries = ["TR", "US"]
204
+
205
+ # Build cache key
206
+ cache_key = f"dovizcom:calendar:{start.date()}:{end.date()}:{','.join(countries)}:{importance or 'all'}"
207
+ cached = self._cache_get(cache_key)
208
+ if cached is not None:
209
+ return cached
210
+
211
+ all_events = []
212
+
213
+ for country_code in countries:
214
+ try:
215
+ # Build params
216
+ importance_param = "3,2,1" # 3=high, 2=mid, 1=low
217
+ if importance == "high":
218
+ importance_param = "3"
219
+ elif importance == "mid":
220
+ importance_param = "3,2"
221
+
222
+ params = {
223
+ "country": country_code,
224
+ "importance": importance_param,
225
+ }
226
+
227
+ response = self._get(
228
+ self.BASE_URL,
229
+ params=params,
230
+ headers=self._get_auth_headers(),
231
+ )
232
+
233
+ data = response.json()
234
+
235
+ if "calendarHTML" not in data:
236
+ continue
237
+
238
+ # Parse HTML
239
+ events = self._parse_html(data["calendarHTML"], country_code)
240
+
241
+ # Filter by date range
242
+ for event in events:
243
+ event_date = event["date"]
244
+ if start.date() <= event_date.date() <= end.date():
245
+ # Filter by importance if specified
246
+ if importance and event["importance"] != importance:
247
+ continue
248
+ all_events.append(event)
249
+
250
+ except Exception as e:
251
+ raise APIError(f"Failed to fetch calendar for {country_code}: {e}") from e
252
+
253
+ # Sort by date and time
254
+ all_events.sort(
255
+ key=lambda x: (
256
+ x["date"],
257
+ x["time"] or "99:99",
258
+ )
259
+ )
260
+
261
+ # Cache result
262
+ self._cache_set(cache_key, all_events, TTL.OHLCV_HISTORY)
263
+
264
+ return all_events
265
+
266
+
267
+ # Singleton instance
268
+ _provider: DovizcomCalendarProvider | None = None
269
+
270
+
271
+ def get_calendar_provider() -> DovizcomCalendarProvider:
272
+ """Get the singleton calendar provider instance."""
273
+ global _provider
274
+ if _provider is None:
275
+ _provider = DovizcomCalendarProvider()
276
+ return _provider
@@ -0,0 +1,172 @@
1
+ """Doviz.com Bond/Tahvil provider for borsapy."""
2
+
3
+ from typing import Any
4
+
5
+ from bs4 import BeautifulSoup
6
+
7
+ from borsapy._providers.base import BaseProvider
8
+ from borsapy.cache import TTL
9
+ from borsapy.exceptions import APIError, DataNotAvailableError
10
+
11
+
12
+ class DovizcomTahvilProvider(BaseProvider):
13
+ """
14
+ Provider for Turkish government bond yields from doviz.com.
15
+
16
+ URL: https://www.doviz.com/tahvil
17
+ """
18
+
19
+ BASE_URL = "https://www.doviz.com/tahvil"
20
+
21
+ # Maturity mapping
22
+ MATURITY_MAP = {
23
+ "2Y": ["2 Yıllık", "2 yıllık", "2-yillik"],
24
+ "5Y": ["5 Yıllık", "5 yıllık", "5-yillik"],
25
+ "10Y": ["10 Yıllık", "10 yıllık", "10-yillik"],
26
+ }
27
+
28
+ def _parse_float(self, text: str) -> float | None:
29
+ """Parse float from Turkish-formatted text."""
30
+ try:
31
+ cleaned = text.strip().replace(",", ".").replace("%", "")
32
+ return float(cleaned)
33
+ except (ValueError, AttributeError):
34
+ return None
35
+
36
+ def _get_maturity(self, name: str) -> str | None:
37
+ """Get maturity code from bond name."""
38
+ for maturity, patterns in self.MATURITY_MAP.items():
39
+ if any(p in name for p in patterns):
40
+ return maturity
41
+ return None
42
+
43
+ def get_bond_yields(self) -> list[dict[str, Any]]:
44
+ """
45
+ Get current Turkish government bond yields.
46
+
47
+ Returns:
48
+ List of bond data with name, maturity, yield, change, etc.
49
+
50
+ Raises:
51
+ APIError: If API request fails.
52
+ DataNotAvailableError: If bond data not found.
53
+ """
54
+ cache_key = "dovizcom:tahvil:all"
55
+ cached = self._cache_get(cache_key)
56
+ if cached is not None:
57
+ return cached
58
+
59
+ try:
60
+ response = self._get(self.BASE_URL)
61
+ soup = BeautifulSoup(response.content, "html.parser")
62
+
63
+ # Find the commodities table
64
+ table = soup.find("table", {"id": "commodities"})
65
+ if not table:
66
+ raise DataNotAvailableError("Bond table not found on page")
67
+
68
+ tbody = table.find("tbody")
69
+ if not tbody:
70
+ raise DataNotAvailableError("Bond data not found")
71
+
72
+ bonds = []
73
+
74
+ for row in tbody.find_all("tr"):
75
+ try:
76
+ cells = row.find_all("td")
77
+ if len(cells) < 3:
78
+ continue
79
+
80
+ # Parse bond name and URL
81
+ name_link = cells[0].find("a", class_="name")
82
+ if not name_link:
83
+ continue
84
+
85
+ name = name_link.text.strip()
86
+ url = name_link.get("href", "")
87
+
88
+ # Parse current yield
89
+ yield_text = cells[1].text.strip()
90
+ yield_rate = self._parse_float(yield_text)
91
+
92
+ # Parse change percentage
93
+ change_text = cells[2].text.strip()
94
+ change_pct = self._parse_float(change_text)
95
+
96
+ # Get maturity
97
+ maturity = self._get_maturity(name)
98
+
99
+ bond_data = {
100
+ "name": name,
101
+ "maturity": maturity,
102
+ "yield": yield_rate,
103
+ "yield_decimal": yield_rate / 100 if yield_rate else None,
104
+ "change": yield_rate * (change_pct / 100) if yield_rate and change_pct else None,
105
+ "change_pct": change_pct,
106
+ "url": url,
107
+ }
108
+
109
+ bonds.append(bond_data)
110
+
111
+ except Exception:
112
+ continue
113
+
114
+ if not bonds:
115
+ raise DataNotAvailableError("No bond data found")
116
+
117
+ self._cache_set(cache_key, bonds, TTL.FX_RATES)
118
+ return bonds
119
+
120
+ except (DataNotAvailableError, APIError):
121
+ raise
122
+ except Exception as e:
123
+ raise APIError(f"Failed to fetch bond yields: {e}") from e
124
+
125
+ def get_bond(self, maturity: str) -> dict[str, Any]:
126
+ """
127
+ Get a specific bond by maturity.
128
+
129
+ Args:
130
+ maturity: Bond maturity (2Y, 5Y, 10Y).
131
+
132
+ Returns:
133
+ Bond data dict.
134
+
135
+ Raises:
136
+ DataNotAvailableError: If bond not found.
137
+ """
138
+ maturity = maturity.upper()
139
+ bonds = self.get_bond_yields()
140
+
141
+ for bond in bonds:
142
+ if bond["maturity"] == maturity:
143
+ return bond
144
+
145
+ raise DataNotAvailableError(f"Bond with maturity {maturity} not found")
146
+
147
+ def get_10y_yield(self) -> float | None:
148
+ """
149
+ Get current 10-year Turkish government bond yield as decimal.
150
+
151
+ Useful for DCF calculations.
152
+
153
+ Returns:
154
+ 10Y bond yield as decimal (e.g., 0.28 for 28%).
155
+ """
156
+ try:
157
+ bond = self.get_bond("10Y")
158
+ return bond.get("yield_decimal")
159
+ except DataNotAvailableError:
160
+ return None
161
+
162
+
163
+ # Singleton instance
164
+ _provider: DovizcomTahvilProvider | None = None
165
+
166
+
167
+ def get_tahvil_provider() -> DovizcomTahvilProvider:
168
+ """Get the singleton tahvil provider instance."""
169
+ global _provider
170
+ if _provider is None:
171
+ _provider = DovizcomTahvilProvider()
172
+ return _provider