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 +134 -0
- borsapy/_models/__init__.py +1 -0
- borsapy/_providers/__init__.py +5 -0
- borsapy/_providers/base.py +94 -0
- borsapy/_providers/bist_index.py +150 -0
- borsapy/_providers/btcturk.py +230 -0
- borsapy/_providers/canlidoviz.py +773 -0
- borsapy/_providers/dovizcom.py +869 -0
- borsapy/_providers/dovizcom_calendar.py +276 -0
- borsapy/_providers/dovizcom_tahvil.py +172 -0
- borsapy/_providers/hedeffiyat.py +376 -0
- borsapy/_providers/isin.py +247 -0
- borsapy/_providers/isyatirim.py +943 -0
- borsapy/_providers/isyatirim_screener.py +468 -0
- borsapy/_providers/kap.py +534 -0
- borsapy/_providers/paratic.py +278 -0
- borsapy/_providers/tcmb.py +317 -0
- borsapy/_providers/tefas.py +802 -0
- borsapy/_providers/viop.py +204 -0
- borsapy/bond.py +162 -0
- borsapy/cache.py +86 -0
- borsapy/calendar.py +272 -0
- borsapy/crypto.py +153 -0
- borsapy/exceptions.py +64 -0
- borsapy/fund.py +471 -0
- borsapy/fx.py +388 -0
- borsapy/index.py +285 -0
- borsapy/inflation.py +166 -0
- borsapy/market.py +53 -0
- borsapy/multi.py +227 -0
- borsapy/screener.py +365 -0
- borsapy/ticker.py +1196 -0
- borsapy/viop.py +162 -0
- borsapy-0.4.0.dist-info/METADATA +969 -0
- borsapy-0.4.0.dist-info/RECORD +37 -0
- borsapy-0.4.0.dist-info/WHEEL +4 -0
- borsapy-0.4.0.dist-info/licenses/LICENSE +190 -0
|
@@ -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
|