borsapy 0.1.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 +92 -0
- borsapy/_models/__init__.py +1 -0
- borsapy/_providers/__init__.py +5 -0
- borsapy/_providers/base.py +94 -0
- borsapy/_providers/btcturk.py +230 -0
- borsapy/_providers/dovizcom.py +282 -0
- borsapy/_providers/hedeffiyat.py +376 -0
- borsapy/_providers/isin.py +247 -0
- borsapy/_providers/isyatirim.py +929 -0
- borsapy/_providers/kap.py +534 -0
- borsapy/_providers/paratic.py +264 -0
- borsapy/_providers/tcmb.py +317 -0
- borsapy/_providers/tefas.py +270 -0
- borsapy/_providers/viop.py +204 -0
- borsapy/cache.py +86 -0
- borsapy/crypto.py +153 -0
- borsapy/exceptions.py +64 -0
- borsapy/fund.py +178 -0
- borsapy/fx.py +128 -0
- borsapy/index.py +185 -0
- borsapy/inflation.py +166 -0
- borsapy/market.py +53 -0
- borsapy/multi.py +227 -0
- borsapy/ticker.py +1192 -0
- borsapy/viop.py +162 -0
- borsapy-0.1.0.dist-info/METADATA +409 -0
- borsapy-0.1.0.dist-info/RECORD +29 -0
- borsapy-0.1.0.dist-info/WHEEL +4 -0
- borsapy-0.1.0.dist-info/licenses/LICENSE +190 -0
borsapy/__init__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
>>> gold = bp.FX("gram-altin")
|
|
20
|
+
|
|
21
|
+
# List all BIST companies
|
|
22
|
+
>>> bp.companies()
|
|
23
|
+
>>> bp.search_companies("banka")
|
|
24
|
+
|
|
25
|
+
# Get crypto data
|
|
26
|
+
>>> btc = bp.Crypto("BTCTRY")
|
|
27
|
+
>>> btc.current # Current price
|
|
28
|
+
>>> btc.history(period="1mo") # Historical OHLCV
|
|
29
|
+
>>> bp.crypto_pairs() # List available pairs
|
|
30
|
+
|
|
31
|
+
# Get fund data
|
|
32
|
+
>>> fund = bp.Fund("AAK")
|
|
33
|
+
>>> fund.info # Fund details
|
|
34
|
+
>>> fund.history(period="1mo") # Price history
|
|
35
|
+
|
|
36
|
+
# Get inflation data
|
|
37
|
+
>>> inf = bp.Inflation()
|
|
38
|
+
>>> inf.latest() # Latest TÜFE data
|
|
39
|
+
>>> inf.calculate(100000, "2020-01", "2024-01") # Inflation calculation
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from borsapy.crypto import Crypto, crypto_pairs
|
|
43
|
+
from borsapy.exceptions import (
|
|
44
|
+
APIError,
|
|
45
|
+
AuthenticationError,
|
|
46
|
+
BorsapyError,
|
|
47
|
+
DataNotAvailableError,
|
|
48
|
+
InvalidIntervalError,
|
|
49
|
+
InvalidPeriodError,
|
|
50
|
+
RateLimitError,
|
|
51
|
+
TickerNotFoundError,
|
|
52
|
+
)
|
|
53
|
+
from borsapy.fund import Fund, search_funds
|
|
54
|
+
from borsapy.fx import FX
|
|
55
|
+
from borsapy.index import Index, index, indices
|
|
56
|
+
from borsapy.inflation import Inflation
|
|
57
|
+
from borsapy.market import companies, search_companies
|
|
58
|
+
from borsapy.multi import Tickers, download
|
|
59
|
+
from borsapy.ticker import Ticker
|
|
60
|
+
from borsapy.viop import VIOP
|
|
61
|
+
|
|
62
|
+
__version__ = "0.1.0"
|
|
63
|
+
__author__ = "Said Surucu"
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
# Main classes
|
|
67
|
+
"Ticker",
|
|
68
|
+
"Tickers",
|
|
69
|
+
"FX",
|
|
70
|
+
"Crypto",
|
|
71
|
+
"Fund",
|
|
72
|
+
"Index",
|
|
73
|
+
"Inflation",
|
|
74
|
+
"VIOP",
|
|
75
|
+
# Market functions
|
|
76
|
+
"companies",
|
|
77
|
+
"search_companies",
|
|
78
|
+
"crypto_pairs",
|
|
79
|
+
"search_funds",
|
|
80
|
+
"download",
|
|
81
|
+
"index",
|
|
82
|
+
"indices",
|
|
83
|
+
# Exceptions
|
|
84
|
+
"BorsapyError",
|
|
85
|
+
"TickerNotFoundError",
|
|
86
|
+
"DataNotAvailableError",
|
|
87
|
+
"APIError",
|
|
88
|
+
"AuthenticationError",
|
|
89
|
+
"RateLimitError",
|
|
90
|
+
"InvalidPeriodError",
|
|
91
|
+
"InvalidIntervalError",
|
|
92
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Data models for borsapy."""
|
|
@@ -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,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
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Doviz.com provider for forex and commodity data."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
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 DovizcomProvider(BaseProvider):
|
|
16
|
+
"""
|
|
17
|
+
Provider for forex and commodity data from doviz.com.
|
|
18
|
+
|
|
19
|
+
Supported assets:
|
|
20
|
+
- Currencies: USD, EUR, GBP, JPY, CHF, CAD, AUD
|
|
21
|
+
- Precious Metals: gram-altin, gumus, ons, XAG-USD, XPT-USD, XPD-USD
|
|
22
|
+
- Energy: BRENT, WTI
|
|
23
|
+
- Fuel: diesel, gasoline, lpg
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
BASE_URL = "https://api.doviz.com/api/v12"
|
|
27
|
+
TOKEN_EXPIRY = 3600 # 1 hour
|
|
28
|
+
|
|
29
|
+
FALLBACK_TOKEN = "3e75d7fabf1c50c8b962626dd0e5ea22d8000815e1b0920d0a26afd77fcd6609"
|
|
30
|
+
|
|
31
|
+
SUPPORTED_ASSETS = {
|
|
32
|
+
# Currencies
|
|
33
|
+
"USD",
|
|
34
|
+
"EUR",
|
|
35
|
+
"GBP",
|
|
36
|
+
"JPY",
|
|
37
|
+
"CHF",
|
|
38
|
+
"CAD",
|
|
39
|
+
"AUD",
|
|
40
|
+
# Precious Metals (TRY)
|
|
41
|
+
"gram-altin",
|
|
42
|
+
"gumus",
|
|
43
|
+
# Precious Metals (USD)
|
|
44
|
+
"ons",
|
|
45
|
+
"XAG-USD",
|
|
46
|
+
"XPT-USD",
|
|
47
|
+
"XPD-USD",
|
|
48
|
+
# Energy
|
|
49
|
+
"BRENT",
|
|
50
|
+
"WTI",
|
|
51
|
+
# Fuel
|
|
52
|
+
"diesel",
|
|
53
|
+
"gasoline",
|
|
54
|
+
"lpg",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
FUEL_ASSETS = {"gasoline", "diesel", "lpg"}
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
super().__init__()
|
|
61
|
+
self._token: str | None = None
|
|
62
|
+
self._token_expiry: float = 0
|
|
63
|
+
|
|
64
|
+
def _get_token(self) -> str:
|
|
65
|
+
"""Get valid Bearer token."""
|
|
66
|
+
if self._token and time.time() < self._token_expiry:
|
|
67
|
+
return self._token
|
|
68
|
+
|
|
69
|
+
# Try to extract token from website
|
|
70
|
+
try:
|
|
71
|
+
token = self._extract_token()
|
|
72
|
+
if token:
|
|
73
|
+
self._token = token
|
|
74
|
+
self._token_expiry = time.time() + self.TOKEN_EXPIRY
|
|
75
|
+
return token
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Use fallback
|
|
80
|
+
return self.FALLBACK_TOKEN
|
|
81
|
+
|
|
82
|
+
def _extract_token(self) -> str | None:
|
|
83
|
+
"""Extract token from doviz.com website."""
|
|
84
|
+
try:
|
|
85
|
+
response = self._client.get("https://www.doviz.com/")
|
|
86
|
+
html = response.text
|
|
87
|
+
|
|
88
|
+
# Look for 64-char hex token
|
|
89
|
+
patterns = [
|
|
90
|
+
r'token["\']?\s*:\s*["\']([a-f0-9]{64})["\']',
|
|
91
|
+
r"Bearer\s+([a-f0-9]{64})",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for pattern in patterns:
|
|
95
|
+
match = re.search(pattern, html, re.IGNORECASE)
|
|
96
|
+
if match:
|
|
97
|
+
return match.group(1)
|
|
98
|
+
|
|
99
|
+
return None
|
|
100
|
+
except Exception:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def _get_headers(self, asset: str) -> dict[str, str]:
|
|
104
|
+
"""Get request headers with token."""
|
|
105
|
+
if asset in ["gram-altin", "gumus", "ons"]:
|
|
106
|
+
origin = "https://altin.doviz.com"
|
|
107
|
+
else:
|
|
108
|
+
origin = "https://www.doviz.com"
|
|
109
|
+
|
|
110
|
+
token = self._get_token()
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"Accept": "*/*",
|
|
114
|
+
"Authorization": f"Bearer {token}",
|
|
115
|
+
"Origin": origin,
|
|
116
|
+
"Referer": f"{origin}/",
|
|
117
|
+
"User-Agent": self.DEFAULT_HEADERS["User-Agent"],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
def get_current(self, asset: str) -> dict[str, Any]:
|
|
121
|
+
"""
|
|
122
|
+
Get current price for an asset.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
asset: Asset code (USD, EUR, gram-altin, BRENT, etc.)
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary with current price data.
|
|
129
|
+
"""
|
|
130
|
+
asset = asset.upper() if asset.upper() in self.SUPPORTED_ASSETS else asset
|
|
131
|
+
|
|
132
|
+
if asset not in self.SUPPORTED_ASSETS:
|
|
133
|
+
raise DataNotAvailableError(
|
|
134
|
+
f"Unsupported asset: {asset}. Supported: {sorted(self.SUPPORTED_ASSETS)}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
cache_key = f"dovizcom:current:{asset}"
|
|
138
|
+
cached = self._cache_get(cache_key)
|
|
139
|
+
if cached:
|
|
140
|
+
return cached
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
if asset in self.FUEL_ASSETS:
|
|
144
|
+
data = self._get_from_archive(asset, days=7)
|
|
145
|
+
else:
|
|
146
|
+
data = self._get_from_daily(asset)
|
|
147
|
+
|
|
148
|
+
if not data:
|
|
149
|
+
raise DataNotAvailableError(f"No data for {asset}")
|
|
150
|
+
|
|
151
|
+
result = {
|
|
152
|
+
"symbol": asset,
|
|
153
|
+
"last": float(data.get("close", 0)),
|
|
154
|
+
"open": float(data.get("open", 0)),
|
|
155
|
+
"high": float(data.get("highest", 0)),
|
|
156
|
+
"low": float(data.get("lowest", 0)),
|
|
157
|
+
"update_time": self._parse_timestamp(data.get("update_date")),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
self._cache_set(cache_key, result, TTL.FX_RATES)
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise APIError(f"Failed to fetch {asset}: {e}") from e
|
|
165
|
+
|
|
166
|
+
def get_history(
|
|
167
|
+
self,
|
|
168
|
+
asset: str,
|
|
169
|
+
period: str = "1mo",
|
|
170
|
+
start: datetime | None = None,
|
|
171
|
+
end: datetime | None = None,
|
|
172
|
+
) -> pd.DataFrame:
|
|
173
|
+
"""
|
|
174
|
+
Get historical data for an asset.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
asset: Asset code.
|
|
178
|
+
period: Period (1d, 5d, 1mo, 3mo, 6mo, 1y).
|
|
179
|
+
start: Start date.
|
|
180
|
+
end: End date.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
DataFrame with OHLC data.
|
|
184
|
+
"""
|
|
185
|
+
asset = asset.upper() if asset.upper() in self.SUPPORTED_ASSETS else asset
|
|
186
|
+
|
|
187
|
+
if asset not in self.SUPPORTED_ASSETS:
|
|
188
|
+
raise DataNotAvailableError(f"Unsupported asset: {asset}")
|
|
189
|
+
|
|
190
|
+
# Calculate date range
|
|
191
|
+
end_dt = end or datetime.now()
|
|
192
|
+
if start:
|
|
193
|
+
start_dt = start
|
|
194
|
+
else:
|
|
195
|
+
days = {"1d": 1, "5d": 5, "1mo": 30, "3mo": 90, "6mo": 180, "1y": 365}.get(period, 30)
|
|
196
|
+
start_dt = end_dt - timedelta(days=days)
|
|
197
|
+
|
|
198
|
+
cache_key = f"dovizcom:history:{asset}:{start_dt.date()}:{end_dt.date()}"
|
|
199
|
+
cached = self._cache_get(cache_key)
|
|
200
|
+
if cached is not None:
|
|
201
|
+
return cached
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
url = f"{self.BASE_URL}/assets/{asset}/archive"
|
|
205
|
+
params = {
|
|
206
|
+
"start": int(start_dt.timestamp()),
|
|
207
|
+
"end": int(end_dt.timestamp()),
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
response = self._client.get(url, headers=self._get_headers(asset), params=params)
|
|
211
|
+
response.raise_for_status()
|
|
212
|
+
data = response.json()
|
|
213
|
+
|
|
214
|
+
archive = data.get("data", {}).get("archive", [])
|
|
215
|
+
|
|
216
|
+
records = []
|
|
217
|
+
for item in archive:
|
|
218
|
+
records.append(
|
|
219
|
+
{
|
|
220
|
+
"Date": self._parse_timestamp(item.get("update_date")),
|
|
221
|
+
"Open": float(item.get("open", 0)),
|
|
222
|
+
"High": float(item.get("highest", 0)),
|
|
223
|
+
"Low": float(item.get("lowest", 0)),
|
|
224
|
+
"Close": float(item.get("close", 0)),
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
df = pd.DataFrame(records)
|
|
229
|
+
if not df.empty:
|
|
230
|
+
df.set_index("Date", inplace=True)
|
|
231
|
+
df.sort_index(inplace=True)
|
|
232
|
+
|
|
233
|
+
self._cache_set(cache_key, df, TTL.OHLCV_HISTORY)
|
|
234
|
+
return df
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
raise APIError(f"Failed to fetch history for {asset}: {e}") from e
|
|
238
|
+
|
|
239
|
+
def _get_from_daily(self, asset: str) -> dict | None:
|
|
240
|
+
"""Get latest data from daily endpoint."""
|
|
241
|
+
url = f"{self.BASE_URL}/assets/{asset}/daily"
|
|
242
|
+
response = self._client.get(url, headers=self._get_headers(asset), params={"limit": 1})
|
|
243
|
+
response.raise_for_status()
|
|
244
|
+
data = response.json()
|
|
245
|
+
|
|
246
|
+
archive = data.get("data", {}).get("archive", [])
|
|
247
|
+
return archive[0] if archive else None
|
|
248
|
+
|
|
249
|
+
def _get_from_archive(self, asset: str, days: int = 7) -> dict | None:
|
|
250
|
+
"""Get latest data from archive endpoint."""
|
|
251
|
+
end_time = int(time.time())
|
|
252
|
+
start_time = end_time - (days * 86400)
|
|
253
|
+
|
|
254
|
+
url = f"{self.BASE_URL}/assets/{asset}/archive"
|
|
255
|
+
params = {"start": start_time, "end": end_time}
|
|
256
|
+
|
|
257
|
+
response = self._client.get(url, headers=self._get_headers(asset), params=params)
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
data = response.json()
|
|
260
|
+
|
|
261
|
+
archive = data.get("data", {}).get("archive", [])
|
|
262
|
+
return archive[-1] if archive else None
|
|
263
|
+
|
|
264
|
+
def _parse_timestamp(self, ts: Any) -> datetime:
|
|
265
|
+
"""Parse timestamp to datetime."""
|
|
266
|
+
if isinstance(ts, (int, float)):
|
|
267
|
+
return datetime.fromtimestamp(ts)
|
|
268
|
+
if isinstance(ts, datetime):
|
|
269
|
+
return ts
|
|
270
|
+
return datetime.now()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Singleton
|
|
274
|
+
_provider: DovizcomProvider | None = None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_dovizcom_provider() -> DovizcomProvider:
|
|
278
|
+
"""Get singleton provider instance."""
|
|
279
|
+
global _provider
|
|
280
|
+
if _provider is None:
|
|
281
|
+
_provider = DovizcomProvider()
|
|
282
|
+
return _provider
|