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,773 @@
|
|
|
1
|
+
"""Canlidoviz.com provider for forex data - token-free alternative to doviz.com."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
|
|
10
|
+
from borsapy._providers.base import BaseProvider
|
|
11
|
+
from borsapy.cache import TTL
|
|
12
|
+
from borsapy.exceptions import APIError, DataNotAvailableError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CanlidovizProvider(BaseProvider):
|
|
16
|
+
"""
|
|
17
|
+
Provider for forex data from canlidoviz.com.
|
|
18
|
+
|
|
19
|
+
Key advantage: No authentication token required!
|
|
20
|
+
|
|
21
|
+
Supports:
|
|
22
|
+
- Currency history (USD, EUR, GBP, etc.)
|
|
23
|
+
- Bank-specific currency rates and history
|
|
24
|
+
- Precious metal rates (gram-altin, etc.)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
API_BASE = "https://a.canlidoviz.com"
|
|
28
|
+
WEB_BASE = "https://canlidoviz.com"
|
|
29
|
+
|
|
30
|
+
# Main currency IDs (TRY prices) - 65 currencies
|
|
31
|
+
# Discovered via Chrome DevTools network inspection on 2026-01-13
|
|
32
|
+
CURRENCY_IDS = {
|
|
33
|
+
# Major currencies
|
|
34
|
+
"USD": 1, # ABD Doları
|
|
35
|
+
"EUR": 50, # Euro
|
|
36
|
+
"GBP": 100, # İngiliz Sterlini
|
|
37
|
+
"CHF": 51, # İsviçre Frangı
|
|
38
|
+
"CAD": 56, # Kanada Doları
|
|
39
|
+
"AUD": 102, # Avustralya Doları
|
|
40
|
+
"JPY": 57, # 100 Japon Yeni
|
|
41
|
+
"NZD": 67, # Yeni Zelanda Doları
|
|
42
|
+
"SGD": 17, # Singapur Doları
|
|
43
|
+
"HKD": 80, # Hong Kong Doları
|
|
44
|
+
"TWD": 9, # Yeni Tayvan Doları
|
|
45
|
+
# European currencies
|
|
46
|
+
"DKK": 54, # Danimarka Kronu
|
|
47
|
+
"SEK": 60, # İsveç Kronu
|
|
48
|
+
"NOK": 99, # Norveç Kronu
|
|
49
|
+
"PLN": 110, # Polonya Zlotisi
|
|
50
|
+
"CZK": 69, # Çek Korunası
|
|
51
|
+
"HUF": 108, # Macar Forinti
|
|
52
|
+
"RON": 77, # Romanya Leyi
|
|
53
|
+
"BGN": 71, # Bulgar Levası
|
|
54
|
+
"HRK": 116, # Hırvat Kunası
|
|
55
|
+
"RSD": 7, # Sırbistan Dinarı
|
|
56
|
+
"BAM": 82, # Bosna Hersek Markı
|
|
57
|
+
"MKD": 21, # Makedon Dinarı
|
|
58
|
+
"ALL": 112, # Arnavutluk Leki
|
|
59
|
+
"MDL": 10, # Moldovya Leusu
|
|
60
|
+
"UAH": 8, # Ukrayna Grivnası
|
|
61
|
+
"BYR": 109, # Belarus Rublesi
|
|
62
|
+
"ISK": 83, # İzlanda Kronası
|
|
63
|
+
# Middle East & Africa
|
|
64
|
+
"AED": 53, # BAE Dirhemi
|
|
65
|
+
"SAR": 61, # Suudi Arabistan Riyali
|
|
66
|
+
"QAR": 5, # Katar Riyali
|
|
67
|
+
"KWD": 104, # Kuveyt Dinarı
|
|
68
|
+
"BHD": 64, # Bahreyn Dinarı
|
|
69
|
+
"OMR": 2, # Umman Riyali
|
|
70
|
+
"JOD": 92, # Ürdün Dinarı
|
|
71
|
+
"IQD": 106, # Irak Dinarı
|
|
72
|
+
"IRR": 68, # İran Riyali
|
|
73
|
+
"LBP": 117, # Lübnan Lirası
|
|
74
|
+
"SYP": 6, # Suriye Lirası
|
|
75
|
+
"EGP": 111, # Mısır Lirası
|
|
76
|
+
"LYD": 101, # Libya Dinarı
|
|
77
|
+
"TND": 885, # Tunus Dinarı
|
|
78
|
+
"DZD": 88, # Cezayir Dinarı
|
|
79
|
+
"MAD": 89, # Fas Dirhemi
|
|
80
|
+
"ZAR": 59, # Güney Afrika Randı
|
|
81
|
+
"ILS": 63, # İsrail Şekeli
|
|
82
|
+
# Asia & Pacific
|
|
83
|
+
"CNY": 107, # Çin Yuanı
|
|
84
|
+
"INR": 103, # Hindistan Rupisi
|
|
85
|
+
"PKR": 29, # Pakistan Rupisi
|
|
86
|
+
"LKR": 87, # Sri Lanka Rupisi
|
|
87
|
+
"IDR": 105, # Endonezya Rupiahı
|
|
88
|
+
"MYR": 3, # Malezya Ringgiti
|
|
89
|
+
"THB": 39, # Tayland Bahtı
|
|
90
|
+
"PHP": 4, # Filipinler Pesosu
|
|
91
|
+
"KRW": 113, # Güney Kore Wonu
|
|
92
|
+
"KZT": 85, # Kazak Tengesi
|
|
93
|
+
"AZN": 75, # Azerbaycan Manatı
|
|
94
|
+
"GEL": 162, # Gürcistan Larisi
|
|
95
|
+
# Americas
|
|
96
|
+
"MXN": 65, # Meksika Pesosu
|
|
97
|
+
"BRL": 74, # Brezilya Reali
|
|
98
|
+
"ARS": 73, # Arjantin Pesosu
|
|
99
|
+
"CLP": 76, # Şili Pesosu
|
|
100
|
+
"COP": 114, # Kolombiya Pesosu
|
|
101
|
+
"PEN": 13, # Peru İnti
|
|
102
|
+
"UYU": 25, # Uruguay Pesosu
|
|
103
|
+
"CRC": 79, # Kostarika Kolonu
|
|
104
|
+
# Other
|
|
105
|
+
"RUB": 97, # Rus Rublesi
|
|
106
|
+
"DVZSP1": 783, # Sepet Kur (Döviz Sepeti)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Precious metal IDs (TRY prices)
|
|
110
|
+
# Note: These IDs were verified against canlidoviz.com pages on 2026-01-13
|
|
111
|
+
METAL_IDS = {
|
|
112
|
+
"gram-altin": 32, # ~6,300 TRY (altin-fiyatlari/gram-altin)
|
|
113
|
+
"ceyrek-altin": 11, # ~10,500 TRY
|
|
114
|
+
"yarim-altin": 47, # ~21,000 TRY
|
|
115
|
+
"tam-altin": 14, # ~42,000 TRY
|
|
116
|
+
"cumhuriyet-altin": 27, # ~43,000 TRY
|
|
117
|
+
"ata-altin": 43, # ~43,000 TRY
|
|
118
|
+
"gram-gumus": 20, # ~115 TRY (altin-fiyatlari/gumus)
|
|
119
|
+
"ons-altin": 81, # ~104,000 TRY (ons in TRY)
|
|
120
|
+
"gram-platin": 1012, # ~3,260 TRY (emtia-fiyatlari/platin-gram)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Energy IDs (USD prices)
|
|
124
|
+
# Verified via Chrome DevTools network inspection on 2026-01-13
|
|
125
|
+
ENERGY_IDS = {
|
|
126
|
+
"BRENT": 266, # Brent Petrol ~$64 (emtia-fiyatlari/brent-petrol)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Commodity IDs - Precious metals in USD (emtia)
|
|
130
|
+
# Verified via Chrome DevTools network inspection on 2026-01-13
|
|
131
|
+
COMMODITY_IDS = {
|
|
132
|
+
"XAG-USD": 267, # Silver Ounce (emtia-fiyatlari/gumus-ons)
|
|
133
|
+
"XPT-USD": 268, # Platinum Spot (emtia-fiyatlari/platin-spot-dolar)
|
|
134
|
+
"XPD-USD": 269, # Palladium Spot (emtia-fiyatlari/paladyum-spot-dolar)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Bank-specific USD IDs
|
|
138
|
+
BANK_USD_IDS = {
|
|
139
|
+
"akbank": 822,
|
|
140
|
+
"garanti-bbva": 805,
|
|
141
|
+
"is-bankasi": 1020,
|
|
142
|
+
"ziraat-bankasi": 264,
|
|
143
|
+
"halkbank": 1017,
|
|
144
|
+
"yapi-kredi": 819,
|
|
145
|
+
"vakifbank": 1018,
|
|
146
|
+
"denizbank": 1019,
|
|
147
|
+
"ing-bank": 1023,
|
|
148
|
+
"hsbc": 1025,
|
|
149
|
+
"teb": 1024,
|
|
150
|
+
"qnb-finansbank": 788,
|
|
151
|
+
"merkez-bankasi": 1016,
|
|
152
|
+
"kapali-carsi": 1114,
|
|
153
|
+
"kuveyt-turk": 1021,
|
|
154
|
+
"albaraka-turk": 1022,
|
|
155
|
+
"sekerbank": 1113,
|
|
156
|
+
"enpara": 824,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# Bank-specific EUR IDs
|
|
160
|
+
BANK_EUR_IDS = {
|
|
161
|
+
"akbank": 1341,
|
|
162
|
+
"garanti-bbva": 807,
|
|
163
|
+
"is-bankasi": 1030,
|
|
164
|
+
"ziraat-bankasi": 894,
|
|
165
|
+
"merkez-bankasi": 1026,
|
|
166
|
+
"halkbank": 1027,
|
|
167
|
+
"yapi-kredi": 820,
|
|
168
|
+
"vakifbank": 1028,
|
|
169
|
+
"denizbank": 1029,
|
|
170
|
+
"ing-bank": 1033,
|
|
171
|
+
"hsbc": 1035,
|
|
172
|
+
"teb": 1034,
|
|
173
|
+
"qnb-finansbank": 789,
|
|
174
|
+
"kapali-carsi": 1115,
|
|
175
|
+
"kuveyt-turk": 1031,
|
|
176
|
+
"albaraka-turk": 1032,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# Bank-specific GBP IDs (18 banka - halkbank hariç veri yok)
|
|
180
|
+
BANK_GBP_IDS = {
|
|
181
|
+
"akbank": 1342,
|
|
182
|
+
"albaraka-turk": 1329,
|
|
183
|
+
"denizbank": 1376,
|
|
184
|
+
"destekbank": 1338,
|
|
185
|
+
"fibabanka": 1410,
|
|
186
|
+
"garanti-bbva": 809,
|
|
187
|
+
"hsbc": 1417,
|
|
188
|
+
"ing-bank": 1427,
|
|
189
|
+
"is-bankasi": 1485,
|
|
190
|
+
"kapali-carsi": 1116,
|
|
191
|
+
"kuveyt-turk": 841,
|
|
192
|
+
"merkez-bankasi": 1036,
|
|
193
|
+
"qnb-finansbank": 791,
|
|
194
|
+
"sekerbank": 1289,
|
|
195
|
+
"teb": 1288,
|
|
196
|
+
"vakifbank": 1460,
|
|
197
|
+
"yapi-kredi": 1475,
|
|
198
|
+
"ziraat-bankasi": 896,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Bank-specific CHF IDs
|
|
202
|
+
BANK_CHF_IDS = {
|
|
203
|
+
"akbank": 1351,
|
|
204
|
+
"albaraka-turk": 1330,
|
|
205
|
+
"denizbank": 1377,
|
|
206
|
+
"is-bankasi": 1489,
|
|
207
|
+
"kapali-carsi": 1199,
|
|
208
|
+
"merkez-bankasi": 1440,
|
|
209
|
+
"vakifbank": 1461,
|
|
210
|
+
"yapi-kredi": 1479,
|
|
211
|
+
"ziraat-bankasi": 902,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# Bank-specific CAD IDs
|
|
215
|
+
BANK_CAD_IDS = {
|
|
216
|
+
"akbank": 1345,
|
|
217
|
+
"is-bankasi": 1490,
|
|
218
|
+
"kapali-carsi": 1204,
|
|
219
|
+
"merkez-bankasi": 1442,
|
|
220
|
+
"ziraat-bankasi": 899,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Bank-specific AUD IDs
|
|
224
|
+
BANK_AUD_IDS = {
|
|
225
|
+
"akbank": 1343,
|
|
226
|
+
"is-bankasi": 1486,
|
|
227
|
+
"kapali-carsi": 1203,
|
|
228
|
+
"merkez-bankasi": 1437,
|
|
229
|
+
"ziraat-bankasi": 897,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Bank-specific JPY IDs (100 Japon Yeni)
|
|
233
|
+
BANK_JPY_IDS = {
|
|
234
|
+
"garanti-bbva": 814,
|
|
235
|
+
"kapali-carsi": 1198,
|
|
236
|
+
"merkez-bankasi": 1455,
|
|
237
|
+
"sekerbank": 1498,
|
|
238
|
+
"vakifbank": 1469,
|
|
239
|
+
"ziraat-bankasi": 1286,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Bank-specific RUB IDs (Rus Rublesi)
|
|
243
|
+
BANK_RUB_IDS = {
|
|
244
|
+
"akbank": 1352,
|
|
245
|
+
"albaraka-turk": 1367,
|
|
246
|
+
"denizbank": 1384,
|
|
247
|
+
"ing-bank": 1436,
|
|
248
|
+
"kapali-carsi": 1206,
|
|
249
|
+
"kuveyt-turk": 831,
|
|
250
|
+
"merkez-bankasi": 1448,
|
|
251
|
+
"qnb-finansbank": 801,
|
|
252
|
+
"vakifbank": 1462,
|
|
253
|
+
"ziraat-bankasi": 901,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# Bank-specific SAR IDs (Suudi Arabistan Riyali)
|
|
257
|
+
BANK_SAR_IDS = {
|
|
258
|
+
"akbank": 1350,
|
|
259
|
+
"denizbank": 1401,
|
|
260
|
+
"hsbc": 1418,
|
|
261
|
+
"ing-bank": 1434,
|
|
262
|
+
"is-bankasi": 1493,
|
|
263
|
+
"kapali-carsi": 1205,
|
|
264
|
+
"kuveyt-turk": 842,
|
|
265
|
+
"merkez-bankasi": 1445,
|
|
266
|
+
"qnb-finansbank": 802,
|
|
267
|
+
"vakifbank": 1463,
|
|
268
|
+
"yapi-kredi": 1483,
|
|
269
|
+
"ziraat-bankasi": 903,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# Bank-specific AED IDs (BAE Dirhemi)
|
|
273
|
+
BANK_AED_IDS = {
|
|
274
|
+
"akbank": 1358,
|
|
275
|
+
"denizbank": 1385,
|
|
276
|
+
"kapali-carsi": 1208,
|
|
277
|
+
"merkez-bankasi": 1454,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Bank-specific CNY IDs (Çin Yuanı)
|
|
281
|
+
BANK_CNY_IDS = {
|
|
282
|
+
"akbank": 1353,
|
|
283
|
+
"kapali-carsi": 1210,
|
|
284
|
+
"merkez-bankasi": 1449,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Bank slug to dovizcom-compatible slug mapping
|
|
288
|
+
BANK_SLUG_MAP = {
|
|
289
|
+
"akbank": "akbank",
|
|
290
|
+
"albaraka-turk": "albaraka",
|
|
291
|
+
"denizbank": "denizbank",
|
|
292
|
+
"destekbank": "destekbank",
|
|
293
|
+
"enpara": "enpara",
|
|
294
|
+
"fibabanka": "fibabanka",
|
|
295
|
+
"garanti-bbva": "garanti",
|
|
296
|
+
"halkbank": "halkbank",
|
|
297
|
+
"hsbc": "hsbc",
|
|
298
|
+
"ing-bank": "ing",
|
|
299
|
+
"is-bankasi": "isbank",
|
|
300
|
+
"kapali-carsi": "kapalicarsi",
|
|
301
|
+
"kuveyt-turk": "kuveytturk",
|
|
302
|
+
"merkez-bankasi": "tcmb",
|
|
303
|
+
"qnb-finansbank": "qnb",
|
|
304
|
+
"sekerbank": "sekerbank",
|
|
305
|
+
"teb": "teb",
|
|
306
|
+
"vakifbank": "vakifbank",
|
|
307
|
+
"yapi-kredi": "yapikredi",
|
|
308
|
+
"ziraat-bankasi": "ziraat",
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
# Reverse mapping (dovizcom slug -> canlidoviz slug)
|
|
312
|
+
DOVIZCOM_TO_CANLIDOVIZ = {v: k for k, v in BANK_SLUG_MAP.items()}
|
|
313
|
+
|
|
314
|
+
# Currency code to URL slug mapping (for HTML scraping)
|
|
315
|
+
CURRENCY_SLUGS = {
|
|
316
|
+
"USD": "dolar",
|
|
317
|
+
"EUR": "euro",
|
|
318
|
+
"GBP": "ingiliz-sterlini",
|
|
319
|
+
"CHF": "isvicre-frangi",
|
|
320
|
+
"CAD": "kanada-dolari",
|
|
321
|
+
"AUD": "avustralya-dolari",
|
|
322
|
+
"JPY": "100-japon-yeni",
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
# Bank-specific metal IDs (gram-altin)
|
|
326
|
+
# Verified via Chrome DevTools network inspection (January 2026)
|
|
327
|
+
BANK_GRAM_ALTIN_IDS: dict[str, int] = {
|
|
328
|
+
"kapali-carsi": 1115,
|
|
329
|
+
"akbank": 823,
|
|
330
|
+
"ziraat-bankasi": 1039,
|
|
331
|
+
"is-bankasi": 1040,
|
|
332
|
+
"vakifbank": 1037,
|
|
333
|
+
"halkbank": 1036,
|
|
334
|
+
"garanti-bankasi": 806,
|
|
335
|
+
"yapi-kredi": 821,
|
|
336
|
+
"denizbank": 1038,
|
|
337
|
+
"albaraka": 1112,
|
|
338
|
+
"destekbank": 1339,
|
|
339
|
+
"enpara": 1041,
|
|
340
|
+
"fibabanka": 1300,
|
|
341
|
+
"hsbc": 1045,
|
|
342
|
+
"ing-bank": 1043,
|
|
343
|
+
"kuveyt-turk": 826,
|
|
344
|
+
"qnb-finansbank": 789,
|
|
345
|
+
"sekerbank": 1042,
|
|
346
|
+
"teb": 1044,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Bank-specific metal IDs (gumus/silver)
|
|
350
|
+
# Verified via Chrome DevTools network inspection (January 2026)
|
|
351
|
+
BANK_GUMUS_IDS: dict[str, int] = {
|
|
352
|
+
"kapali-carsi": 1181,
|
|
353
|
+
"akbank": 1359,
|
|
354
|
+
"albaraka": 1372,
|
|
355
|
+
"denizbank": 1378,
|
|
356
|
+
"destekbank": 1340,
|
|
357
|
+
"fibabanka": 1413,
|
|
358
|
+
"garanti-bankasi": 1415,
|
|
359
|
+
"halkbank": 1416,
|
|
360
|
+
"hsbc": 1426,
|
|
361
|
+
"kuveyt-turk": 827,
|
|
362
|
+
"qnb-finansbank": 1456,
|
|
363
|
+
"vakifbank": 1474,
|
|
364
|
+
"ziraat-bankasi": 1283,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
# Bank-specific metal IDs (platin/platinum)
|
|
368
|
+
# Verified via Chrome DevTools network inspection (January 2026)
|
|
369
|
+
# Note: Only Kuveyt Türk provides platin institution rates on canlidoviz
|
|
370
|
+
BANK_PLATIN_IDS: dict[str, int] = {
|
|
371
|
+
"kuveyt-turk": 1013,
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def __init__(self):
|
|
375
|
+
super().__init__()
|
|
376
|
+
|
|
377
|
+
def _get_headers(self) -> dict[str, str]:
|
|
378
|
+
"""Get request headers - no token needed!"""
|
|
379
|
+
return {
|
|
380
|
+
"Accept": "*/*",
|
|
381
|
+
"Origin": self.WEB_BASE,
|
|
382
|
+
"Referer": f"{self.WEB_BASE}/",
|
|
383
|
+
"User-Agent": self.DEFAULT_HEADERS["User-Agent"],
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
def _get_item_id(
|
|
387
|
+
self, asset: str, institution: str | None = None
|
|
388
|
+
) -> int | None:
|
|
389
|
+
"""
|
|
390
|
+
Get canlidoviz item ID for an asset.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
asset: Currency code (USD, EUR) or metal (gram-altin).
|
|
394
|
+
institution: Optional bank/institution slug.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Item ID or None if not found.
|
|
398
|
+
"""
|
|
399
|
+
asset_upper = asset.upper()
|
|
400
|
+
|
|
401
|
+
if institution:
|
|
402
|
+
# Convert dovizcom slug to canlidoviz slug if needed
|
|
403
|
+
inst_slug = self.DOVIZCOM_TO_CANLIDOVIZ.get(institution, institution)
|
|
404
|
+
|
|
405
|
+
# Bank-specific ID
|
|
406
|
+
if asset_upper == "USD":
|
|
407
|
+
return self.BANK_USD_IDS.get(inst_slug)
|
|
408
|
+
elif asset_upper == "EUR":
|
|
409
|
+
return self.BANK_EUR_IDS.get(inst_slug)
|
|
410
|
+
elif asset_upper == "GBP":
|
|
411
|
+
return self.BANK_GBP_IDS.get(inst_slug)
|
|
412
|
+
elif asset_upper == "CHF":
|
|
413
|
+
return self.BANK_CHF_IDS.get(inst_slug)
|
|
414
|
+
elif asset_upper == "CAD":
|
|
415
|
+
return self.BANK_CAD_IDS.get(inst_slug)
|
|
416
|
+
elif asset_upper == "AUD":
|
|
417
|
+
return self.BANK_AUD_IDS.get(inst_slug)
|
|
418
|
+
elif asset_upper == "JPY":
|
|
419
|
+
return self.BANK_JPY_IDS.get(inst_slug)
|
|
420
|
+
elif asset_upper == "RUB":
|
|
421
|
+
return self.BANK_RUB_IDS.get(inst_slug)
|
|
422
|
+
elif asset_upper == "SAR":
|
|
423
|
+
return self.BANK_SAR_IDS.get(inst_slug)
|
|
424
|
+
elif asset_upper == "AED":
|
|
425
|
+
return self.BANK_AED_IDS.get(inst_slug)
|
|
426
|
+
elif asset_upper == "CNY":
|
|
427
|
+
return self.BANK_CNY_IDS.get(inst_slug)
|
|
428
|
+
elif asset == "gram-altin":
|
|
429
|
+
return self.BANK_GRAM_ALTIN_IDS.get(inst_slug)
|
|
430
|
+
elif asset == "gumus":
|
|
431
|
+
return self.BANK_GUMUS_IDS.get(inst_slug)
|
|
432
|
+
elif asset == "gram-platin":
|
|
433
|
+
return self.BANK_PLATIN_IDS.get(inst_slug)
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# Main asset ID
|
|
437
|
+
if asset_upper in self.CURRENCY_IDS:
|
|
438
|
+
return self.CURRENCY_IDS[asset_upper]
|
|
439
|
+
if asset in self.METAL_IDS:
|
|
440
|
+
return self.METAL_IDS[asset]
|
|
441
|
+
if asset_upper in self.ENERGY_IDS:
|
|
442
|
+
return self.ENERGY_IDS[asset_upper]
|
|
443
|
+
if asset_upper in self.COMMODITY_IDS:
|
|
444
|
+
return self.COMMODITY_IDS[asset_upper]
|
|
445
|
+
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
def get_history(
|
|
449
|
+
self,
|
|
450
|
+
asset: str,
|
|
451
|
+
period: str = "1mo",
|
|
452
|
+
start: datetime | None = None,
|
|
453
|
+
end: datetime | None = None,
|
|
454
|
+
institution: str | None = None,
|
|
455
|
+
) -> pd.DataFrame:
|
|
456
|
+
"""
|
|
457
|
+
Get historical OHLC data for a currency or metal.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
asset: Currency code (USD, EUR) or metal (gram-altin).
|
|
461
|
+
period: Period (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y).
|
|
462
|
+
start: Start date. Overrides period if provided.
|
|
463
|
+
end: End date. Defaults to now.
|
|
464
|
+
institution: Optional bank slug for bank-specific history.
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
DataFrame with OHLC data indexed by Date.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
DataNotAvailableError: If asset is not supported.
|
|
471
|
+
APIError: If API request fails.
|
|
472
|
+
"""
|
|
473
|
+
item_id = self._get_item_id(asset, institution)
|
|
474
|
+
if item_id is None:
|
|
475
|
+
if institution:
|
|
476
|
+
raise DataNotAvailableError(
|
|
477
|
+
f"No canlidoviz data for {asset} from {institution}"
|
|
478
|
+
)
|
|
479
|
+
raise DataNotAvailableError(f"Unsupported asset: {asset}")
|
|
480
|
+
|
|
481
|
+
# Calculate date range
|
|
482
|
+
end_dt = end or datetime.now()
|
|
483
|
+
if start:
|
|
484
|
+
start_dt = start
|
|
485
|
+
else:
|
|
486
|
+
days = {
|
|
487
|
+
"1d": 1,
|
|
488
|
+
"5d": 5,
|
|
489
|
+
"1mo": 30,
|
|
490
|
+
"3mo": 90,
|
|
491
|
+
"6mo": 180,
|
|
492
|
+
"1y": 365,
|
|
493
|
+
"2y": 730,
|
|
494
|
+
"5y": 1825,
|
|
495
|
+
"max": 3650,
|
|
496
|
+
}.get(period, 30)
|
|
497
|
+
start_dt = end_dt - timedelta(days=days)
|
|
498
|
+
|
|
499
|
+
cache_key = f"canlidoviz:history:{asset}:{institution}:{start_dt.date()}:{end_dt.date()}"
|
|
500
|
+
cached = self._cache_get(cache_key)
|
|
501
|
+
if cached is not None:
|
|
502
|
+
return cached
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
url = f"{self.API_BASE}/items/history"
|
|
506
|
+
params = {
|
|
507
|
+
"period": "DAILY",
|
|
508
|
+
"itemDataId": item_id,
|
|
509
|
+
"startDate": start_dt.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
510
|
+
"endDate": end_dt.strftime("%Y-%m-%dT%H:%M:%S"),
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
response = self._client.get(url, headers=self._get_headers(), params=params)
|
|
514
|
+
response.raise_for_status()
|
|
515
|
+
data = response.json()
|
|
516
|
+
|
|
517
|
+
# Parse response: {"timestamp": "open|high|low|close", ...}
|
|
518
|
+
records = []
|
|
519
|
+
for ts_str, ohlc_str in data.items():
|
|
520
|
+
try:
|
|
521
|
+
ts = int(ts_str)
|
|
522
|
+
dt = datetime.fromtimestamp(ts)
|
|
523
|
+
values = ohlc_str.split("|")
|
|
524
|
+
if len(values) >= 4:
|
|
525
|
+
records.append({
|
|
526
|
+
"Date": dt,
|
|
527
|
+
"Open": float(values[0]),
|
|
528
|
+
"High": float(values[1]),
|
|
529
|
+
"Low": float(values[2]),
|
|
530
|
+
"Close": float(values[3]),
|
|
531
|
+
})
|
|
532
|
+
except (ValueError, IndexError):
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
df = pd.DataFrame(records)
|
|
536
|
+
if not df.empty:
|
|
537
|
+
df.set_index("Date", inplace=True)
|
|
538
|
+
df.sort_index(inplace=True)
|
|
539
|
+
|
|
540
|
+
self._cache_set(cache_key, df, TTL.OHLCV_HISTORY)
|
|
541
|
+
return df
|
|
542
|
+
|
|
543
|
+
except Exception as e:
|
|
544
|
+
raise APIError(f"Failed to fetch canlidoviz history for {asset}: {e}") from e
|
|
545
|
+
|
|
546
|
+
def get_current(self, asset: str, institution: str | None = None) -> dict[str, Any]:
|
|
547
|
+
"""
|
|
548
|
+
Get current price for a currency or metal.
|
|
549
|
+
|
|
550
|
+
Uses the most recent data point from history API.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
asset: Currency code (USD, EUR) or metal (gram-altin).
|
|
554
|
+
institution: Optional bank slug for bank-specific rate.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
Dictionary with current price data.
|
|
558
|
+
"""
|
|
559
|
+
item_id = self._get_item_id(asset, institution)
|
|
560
|
+
if item_id is None:
|
|
561
|
+
raise DataNotAvailableError(f"Unsupported asset: {asset}")
|
|
562
|
+
|
|
563
|
+
cache_key = f"canlidoviz:current:{asset}:{institution}"
|
|
564
|
+
cached = self._cache_get(cache_key)
|
|
565
|
+
if cached:
|
|
566
|
+
return cached
|
|
567
|
+
|
|
568
|
+
try:
|
|
569
|
+
# Get recent history to extract latest price
|
|
570
|
+
df = self.get_history(asset, period="5d", institution=institution)
|
|
571
|
+
|
|
572
|
+
if df.empty:
|
|
573
|
+
raise DataNotAvailableError(f"No data for {asset}")
|
|
574
|
+
|
|
575
|
+
# Get the most recent row
|
|
576
|
+
latest = df.iloc[-1]
|
|
577
|
+
latest_date = df.index[-1]
|
|
578
|
+
|
|
579
|
+
result = {
|
|
580
|
+
"symbol": asset,
|
|
581
|
+
"last": float(latest["Close"]),
|
|
582
|
+
"open": float(latest["Open"]),
|
|
583
|
+
"high": float(latest["High"]),
|
|
584
|
+
"low": float(latest["Low"]),
|
|
585
|
+
"update_time": latest_date,
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if institution:
|
|
589
|
+
result["institution"] = institution
|
|
590
|
+
|
|
591
|
+
self._cache_set(cache_key, result, TTL.FX_RATES)
|
|
592
|
+
return result
|
|
593
|
+
|
|
594
|
+
except Exception as e:
|
|
595
|
+
raise APIError(f"Failed to fetch current price for {asset}: {e}") from e
|
|
596
|
+
|
|
597
|
+
def get_supported_currencies(self) -> list[str]:
|
|
598
|
+
"""Get list of supported currencies."""
|
|
599
|
+
return sorted(self.CURRENCY_IDS.keys())
|
|
600
|
+
|
|
601
|
+
def get_supported_metals(self) -> list[str]:
|
|
602
|
+
"""Get list of supported metals."""
|
|
603
|
+
return sorted(self.METAL_IDS.keys())
|
|
604
|
+
|
|
605
|
+
def get_supported_banks(self, currency: str = "USD") -> list[str]:
|
|
606
|
+
"""
|
|
607
|
+
Get list of supported banks for a currency.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
currency: Currency code (default USD).
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
List of bank slugs.
|
|
614
|
+
"""
|
|
615
|
+
currency = currency.upper()
|
|
616
|
+
if currency == "USD":
|
|
617
|
+
return sorted(self.BANK_USD_IDS.keys())
|
|
618
|
+
elif currency == "EUR":
|
|
619
|
+
return sorted(self.BANK_EUR_IDS.keys())
|
|
620
|
+
elif currency == "GBP":
|
|
621
|
+
return sorted(self.BANK_GBP_IDS.keys())
|
|
622
|
+
elif currency == "CHF":
|
|
623
|
+
return sorted(self.BANK_CHF_IDS.keys())
|
|
624
|
+
elif currency == "CAD":
|
|
625
|
+
return sorted(self.BANK_CAD_IDS.keys())
|
|
626
|
+
elif currency == "AUD":
|
|
627
|
+
return sorted(self.BANK_AUD_IDS.keys())
|
|
628
|
+
elif currency == "JPY":
|
|
629
|
+
return sorted(self.BANK_JPY_IDS.keys())
|
|
630
|
+
elif currency == "RUB":
|
|
631
|
+
return sorted(self.BANK_RUB_IDS.keys())
|
|
632
|
+
elif currency == "SAR":
|
|
633
|
+
return sorted(self.BANK_SAR_IDS.keys())
|
|
634
|
+
elif currency == "AED":
|
|
635
|
+
return sorted(self.BANK_AED_IDS.keys())
|
|
636
|
+
elif currency == "CNY":
|
|
637
|
+
return sorted(self.BANK_CNY_IDS.keys())
|
|
638
|
+
return []
|
|
639
|
+
|
|
640
|
+
def get_bank_rates(
|
|
641
|
+
self, currency: str, bank: str | None = None
|
|
642
|
+
) -> pd.DataFrame | dict[str, Any]:
|
|
643
|
+
"""
|
|
644
|
+
Get buy/sell rates from banks via HTML scraping.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
currency: Currency code (USD, EUR, GBP, etc.)
|
|
648
|
+
bank: Optional bank slug. If provided, returns single dict.
|
|
649
|
+
|
|
650
|
+
Returns:
|
|
651
|
+
DataFrame with all banks or dict for single bank.
|
|
652
|
+
Columns: bank, bank_name, currency, buy, sell, spread
|
|
653
|
+
|
|
654
|
+
Raises:
|
|
655
|
+
DataNotAvailableError: If currency is not supported.
|
|
656
|
+
"""
|
|
657
|
+
currency = currency.upper()
|
|
658
|
+
slug = self.CURRENCY_SLUGS.get(currency)
|
|
659
|
+
if not slug:
|
|
660
|
+
raise DataNotAvailableError(
|
|
661
|
+
f"Bank rates not available for {currency}. "
|
|
662
|
+
f"Supported: {list(self.CURRENCY_SLUGS.keys())}"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
cache_key = f"canlidoviz:bank_rates:{currency}"
|
|
666
|
+
cached = self._cache_get(cache_key)
|
|
667
|
+
|
|
668
|
+
if cached is None:
|
|
669
|
+
url = f"{self.WEB_BASE}/doviz-kurlari/{slug}"
|
|
670
|
+
try:
|
|
671
|
+
response = self._client.get(url, headers=self._get_headers())
|
|
672
|
+
response.raise_for_status()
|
|
673
|
+
html = response.text
|
|
674
|
+
cached = self._parse_bank_rates_html(html, currency)
|
|
675
|
+
self._cache_set(cache_key, cached, TTL.FX_RATES)
|
|
676
|
+
except Exception as e:
|
|
677
|
+
raise APIError(f"Failed to fetch bank rates: {e}") from e
|
|
678
|
+
|
|
679
|
+
if bank:
|
|
680
|
+
# Convert dovizcom slug to canlidoviz slug
|
|
681
|
+
bank_slug = self.DOVIZCOM_TO_CANLIDOVIZ.get(bank, bank)
|
|
682
|
+
for row in cached:
|
|
683
|
+
if row["bank"] == bank_slug:
|
|
684
|
+
return row
|
|
685
|
+
raise DataNotAvailableError(f"Bank {bank} not found for {currency}")
|
|
686
|
+
|
|
687
|
+
return pd.DataFrame(cached)
|
|
688
|
+
|
|
689
|
+
def _parse_bank_rates_html(
|
|
690
|
+
self, html: str, currency: str
|
|
691
|
+
) -> list[dict[str, Any]]:
|
|
692
|
+
"""Parse bank rates from HTML page.
|
|
693
|
+
|
|
694
|
+
HTML structure: Each bank is in a table row with TDs:
|
|
695
|
+
- TD 0: Bank name link
|
|
696
|
+
- TD 1: Buy price (Alış)
|
|
697
|
+
- TD 2: Sell price + change values concatenated (Satış0.54%-1.21)
|
|
698
|
+
- TD 3: Close (Kapanış)
|
|
699
|
+
- TD 4: High (Yüksek)
|
|
700
|
+
- TD 5: Low (Düşük)
|
|
701
|
+
"""
|
|
702
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
703
|
+
results = []
|
|
704
|
+
|
|
705
|
+
# Find all bank links in the "DİĞER PİYASALAR" table
|
|
706
|
+
# Pattern: /doviz-kurlari/{bank-slug}/{currency-slug}
|
|
707
|
+
currency_slug = self.CURRENCY_SLUGS.get(currency.upper(), "")
|
|
708
|
+
pattern = re.compile(rf"/doviz-kurlari/([^/]+)/{currency_slug}$")
|
|
709
|
+
|
|
710
|
+
for link in soup.find_all("a", href=pattern):
|
|
711
|
+
match = pattern.search(link.get("href", ""))
|
|
712
|
+
if not match:
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
bank_slug = match.group(1)
|
|
716
|
+
# Skip the main currency page link
|
|
717
|
+
if bank_slug == currency_slug:
|
|
718
|
+
continue
|
|
719
|
+
|
|
720
|
+
# Get bank display name from link text
|
|
721
|
+
bank_text = link.get_text(strip=True)
|
|
722
|
+
# Remove timestamp if present (e.g., "AKBANK15:57:42" or "AKBANK 15:57:42")
|
|
723
|
+
bank_name = re.sub(r"\s*\d{2}:\d{2}:\d{2}$", "", bank_text)
|
|
724
|
+
|
|
725
|
+
# Find the parent TD element
|
|
726
|
+
td_parent = link.find_parent("td")
|
|
727
|
+
if not td_parent:
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
# Get sibling TDs containing the values
|
|
731
|
+
sibling_tds = td_parent.find_next_siblings("td")
|
|
732
|
+
if len(sibling_tds) < 2:
|
|
733
|
+
continue
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
# TD 0: Buy price (clean number like "42.4400")
|
|
737
|
+
buy_text = sibling_tds[0].get_text(strip=True)
|
|
738
|
+
buy = float(buy_text.replace(",", "."))
|
|
739
|
+
|
|
740
|
+
# TD 1: Sell price + change concatenated (e.g., "43.79000.54%-1.21")
|
|
741
|
+
# Extract the first decimal number (sell price)
|
|
742
|
+
sell_text = sibling_tds[1].get_text(strip=True)
|
|
743
|
+
sell_match = re.match(r"^(\d+[.,]\d+)", sell_text)
|
|
744
|
+
if not sell_match:
|
|
745
|
+
continue
|
|
746
|
+
sell = float(sell_match.group(1).replace(",", "."))
|
|
747
|
+
|
|
748
|
+
spread = round((sell - buy) / buy * 100, 2) if buy > 0 else 0
|
|
749
|
+
|
|
750
|
+
results.append({
|
|
751
|
+
"bank": bank_slug,
|
|
752
|
+
"bank_name": bank_name,
|
|
753
|
+
"currency": currency,
|
|
754
|
+
"buy": buy,
|
|
755
|
+
"sell": sell,
|
|
756
|
+
"spread": spread,
|
|
757
|
+
})
|
|
758
|
+
except (ValueError, IndexError, AttributeError):
|
|
759
|
+
continue
|
|
760
|
+
|
|
761
|
+
return results
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# Singleton
|
|
765
|
+
_provider: CanlidovizProvider | None = None
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def get_canlidoviz_provider() -> CanlidovizProvider:
|
|
769
|
+
"""Get singleton provider instance."""
|
|
770
|
+
global _provider
|
|
771
|
+
if _provider is None:
|
|
772
|
+
_provider = CanlidovizProvider()
|
|
773
|
+
return _provider
|