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
borsapy/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
>>> usd.bank_rates # Bank exchange rates
|
|
20
|
+
>>> usd.bank_rate("akbank") # Single bank rate
|
|
21
|
+
>>> bp.banks() # List supported banks
|
|
22
|
+
>>> gold = bp.FX("gram-altin")
|
|
23
|
+
|
|
24
|
+
# List all BIST companies
|
|
25
|
+
>>> bp.companies()
|
|
26
|
+
>>> bp.search_companies("banka")
|
|
27
|
+
|
|
28
|
+
# Get crypto data
|
|
29
|
+
>>> btc = bp.Crypto("BTCTRY")
|
|
30
|
+
>>> btc.current # Current price
|
|
31
|
+
>>> btc.history(period="1mo") # Historical OHLCV
|
|
32
|
+
>>> bp.crypto_pairs() # List available pairs
|
|
33
|
+
|
|
34
|
+
# Get fund data
|
|
35
|
+
>>> fund = bp.Fund("AAK")
|
|
36
|
+
>>> fund.info # Fund details
|
|
37
|
+
>>> fund.history(period="1mo") # Price history
|
|
38
|
+
|
|
39
|
+
# Get inflation data
|
|
40
|
+
>>> inf = bp.Inflation()
|
|
41
|
+
>>> inf.latest() # Latest TÜFE data
|
|
42
|
+
>>> inf.calculate(100000, "2020-01", "2024-01") # Inflation calculation
|
|
43
|
+
|
|
44
|
+
# Economic calendar
|
|
45
|
+
>>> cal = bp.EconomicCalendar()
|
|
46
|
+
>>> cal.events(period="1w") # This week's events
|
|
47
|
+
>>> cal.today() # Today's events
|
|
48
|
+
>>> bp.economic_calendar(country="TR", importance="high")
|
|
49
|
+
|
|
50
|
+
# Government bonds
|
|
51
|
+
>>> bp.bonds() # All bond yields
|
|
52
|
+
>>> bond = bp.Bond("10Y")
|
|
53
|
+
>>> bond.yield_rate # Current 10Y yield
|
|
54
|
+
>>> bp.risk_free_rate() # For DCF calculations
|
|
55
|
+
|
|
56
|
+
# Stock screener
|
|
57
|
+
>>> bp.screen_stocks(template="high_dividend")
|
|
58
|
+
>>> bp.screen_stocks(market_cap_min=1000, pe_max=15)
|
|
59
|
+
>>> screener = bp.Screener()
|
|
60
|
+
>>> screener.add_filter("dividend_yield", min=3).run()
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
from borsapy.bond import Bond, bonds, risk_free_rate
|
|
64
|
+
from borsapy.calendar import EconomicCalendar, economic_calendar
|
|
65
|
+
from borsapy.crypto import Crypto, crypto_pairs
|
|
66
|
+
from borsapy.exceptions import (
|
|
67
|
+
APIError,
|
|
68
|
+
AuthenticationError,
|
|
69
|
+
BorsapyError,
|
|
70
|
+
DataNotAvailableError,
|
|
71
|
+
InvalidIntervalError,
|
|
72
|
+
InvalidPeriodError,
|
|
73
|
+
RateLimitError,
|
|
74
|
+
TickerNotFoundError,
|
|
75
|
+
)
|
|
76
|
+
from borsapy.fund import Fund, compare_funds, screen_funds, search_funds
|
|
77
|
+
from borsapy.fx import FX, banks, metal_institutions
|
|
78
|
+
from borsapy.index import Index, all_indices, index, indices
|
|
79
|
+
from borsapy.inflation import Inflation
|
|
80
|
+
from borsapy.market import companies, search_companies
|
|
81
|
+
from borsapy.multi import Tickers, download
|
|
82
|
+
from borsapy.screener import Screener, screen_stocks, screener_criteria, sectors, stock_indices
|
|
83
|
+
from borsapy.ticker import Ticker
|
|
84
|
+
from borsapy.viop import VIOP
|
|
85
|
+
|
|
86
|
+
__version__ = "0.4.0"
|
|
87
|
+
__author__ = "Said Surucu"
|
|
88
|
+
|
|
89
|
+
__all__ = [
|
|
90
|
+
# Main classes
|
|
91
|
+
"Ticker",
|
|
92
|
+
"Tickers",
|
|
93
|
+
"FX",
|
|
94
|
+
"Crypto",
|
|
95
|
+
"Fund",
|
|
96
|
+
"Index",
|
|
97
|
+
"Inflation",
|
|
98
|
+
"VIOP",
|
|
99
|
+
"Bond",
|
|
100
|
+
"EconomicCalendar",
|
|
101
|
+
"Screener",
|
|
102
|
+
# Market functions
|
|
103
|
+
"companies",
|
|
104
|
+
"search_companies",
|
|
105
|
+
"banks",
|
|
106
|
+
"metal_institutions",
|
|
107
|
+
"crypto_pairs",
|
|
108
|
+
"search_funds",
|
|
109
|
+
"screen_funds",
|
|
110
|
+
"compare_funds",
|
|
111
|
+
"download",
|
|
112
|
+
"index",
|
|
113
|
+
"indices",
|
|
114
|
+
"all_indices",
|
|
115
|
+
# Bond functions
|
|
116
|
+
"bonds",
|
|
117
|
+
"risk_free_rate",
|
|
118
|
+
# Calendar functions
|
|
119
|
+
"economic_calendar",
|
|
120
|
+
# Screener functions
|
|
121
|
+
"screen_stocks",
|
|
122
|
+
"screener_criteria",
|
|
123
|
+
"sectors",
|
|
124
|
+
"stock_indices",
|
|
125
|
+
# Exceptions
|
|
126
|
+
"BorsapyError",
|
|
127
|
+
"TickerNotFoundError",
|
|
128
|
+
"DataNotAvailableError",
|
|
129
|
+
"APIError",
|
|
130
|
+
"AuthenticationError",
|
|
131
|
+
"RateLimitError",
|
|
132
|
+
"InvalidPeriodError",
|
|
133
|
+
"InvalidIntervalError",
|
|
134
|
+
]
|
|
@@ -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,150 @@
|
|
|
1
|
+
"""BIST index constituent provider - downloads index components from BIST CSV."""
|
|
2
|
+
|
|
3
|
+
from io import StringIO
|
|
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
|
+
|
|
11
|
+
# BIST index components CSV URL
|
|
12
|
+
INDEX_COMPONENTS_URL = "https://www.borsaistanbul.com/datum/hisse_endeks_ds.csv"
|
|
13
|
+
|
|
14
|
+
# Singleton instance
|
|
15
|
+
_provider: "BistIndexProvider | None" = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_bist_index_provider() -> "BistIndexProvider":
|
|
19
|
+
"""Get or create the singleton BistIndexProvider instance."""
|
|
20
|
+
global _provider
|
|
21
|
+
if _provider is None:
|
|
22
|
+
_provider = BistIndexProvider()
|
|
23
|
+
return _provider
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BistIndexProvider(BaseProvider):
|
|
27
|
+
"""Provider for BIST index constituents from official CSV file."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
super().__init__(timeout=30.0)
|
|
31
|
+
self._df_cache: pd.DataFrame | None = None
|
|
32
|
+
|
|
33
|
+
def _download_components(self) -> pd.DataFrame | None:
|
|
34
|
+
"""Download and cache the components CSV."""
|
|
35
|
+
if self._df_cache is not None:
|
|
36
|
+
return self._df_cache
|
|
37
|
+
|
|
38
|
+
# Check memory cache first
|
|
39
|
+
cache_key = "bist:index:components:all"
|
|
40
|
+
cached = self._cache_get(cache_key)
|
|
41
|
+
if cached is not None:
|
|
42
|
+
self._df_cache = cached
|
|
43
|
+
return cached
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
response = self._get(INDEX_COMPONENTS_URL)
|
|
47
|
+
df = pd.read_csv(StringIO(response.text), sep=";")
|
|
48
|
+
# Skip header row (has English column names)
|
|
49
|
+
df = df.iloc[1:]
|
|
50
|
+
# Clean up symbol codes (remove .E suffix)
|
|
51
|
+
df["symbol"] = df["BILESEN KODU"].str.replace(r"\.E$", "", regex=True)
|
|
52
|
+
df["name"] = df["BULTEN_ADI"]
|
|
53
|
+
df["index_code"] = df["ENDEKS KODU"]
|
|
54
|
+
df["index_name"] = df["ENDEKS ADI"]
|
|
55
|
+
|
|
56
|
+
self._df_cache = df
|
|
57
|
+
self._cache_set(cache_key, df, TTL.COMPANY_LIST)
|
|
58
|
+
return df
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
def get_components(self, symbol: str) -> list[dict[str, Any]]:
|
|
63
|
+
"""
|
|
64
|
+
Get constituent stocks for an index.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
symbol: Index symbol (e.g., "XU100", "XU030", "XKTUM").
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of component dicts with 'symbol' and 'name' keys.
|
|
71
|
+
Empty list if index not found or fetch fails.
|
|
72
|
+
|
|
73
|
+
Examples:
|
|
74
|
+
>>> provider = get_bist_index_provider()
|
|
75
|
+
>>> provider.get_components("XU030")
|
|
76
|
+
[{'symbol': 'AKBNK', 'name': 'AKBANK'}, ...]
|
|
77
|
+
"""
|
|
78
|
+
symbol = symbol.upper()
|
|
79
|
+
|
|
80
|
+
df = self._download_components()
|
|
81
|
+
if df is None:
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
# Filter by index code
|
|
85
|
+
mask = df["index_code"] == symbol
|
|
86
|
+
components = df[mask][["symbol", "name"]].to_dict("records")
|
|
87
|
+
|
|
88
|
+
return components
|
|
89
|
+
|
|
90
|
+
def get_available_indices(self) -> list[dict[str, Any]]:
|
|
91
|
+
"""
|
|
92
|
+
Get list of all indices with component counts.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
List of dicts with 'symbol', 'name', and 'count' keys.
|
|
96
|
+
"""
|
|
97
|
+
df = self._download_components()
|
|
98
|
+
if df is None:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
# Group by index code
|
|
102
|
+
grouped = df.groupby(["index_code", "index_name"]).size().reset_index(name="count")
|
|
103
|
+
indices = [
|
|
104
|
+
{"symbol": row["index_code"], "name": row["index_name"], "count": row["count"]}
|
|
105
|
+
for _, row in grouped.iterrows()
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return sorted(indices, key=lambda x: x["symbol"])
|
|
109
|
+
|
|
110
|
+
def is_in_index(self, ticker: str, index_symbol: str) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Check if a stock is in a specific index.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
ticker: Stock symbol (e.g., "THYAO").
|
|
116
|
+
index_symbol: Index symbol (e.g., "XU030").
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
True if stock is in the index.
|
|
120
|
+
"""
|
|
121
|
+
ticker = ticker.upper()
|
|
122
|
+
index_symbol = index_symbol.upper()
|
|
123
|
+
|
|
124
|
+
df = self._download_components()
|
|
125
|
+
if df is None:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
mask = (df["symbol"] == ticker) & (df["index_code"] == index_symbol)
|
|
129
|
+
return mask.any()
|
|
130
|
+
|
|
131
|
+
def get_indices_for_ticker(self, ticker: str) -> list[str]:
|
|
132
|
+
"""
|
|
133
|
+
Get all indices that contain a specific stock.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
ticker: Stock symbol (e.g., "THYAO").
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of index symbols that contain this stock.
|
|
140
|
+
"""
|
|
141
|
+
ticker = ticker.upper()
|
|
142
|
+
|
|
143
|
+
df = self._download_components()
|
|
144
|
+
if df is None:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
mask = df["symbol"] == ticker
|
|
148
|
+
indices = df[mask]["index_code"].unique().tolist()
|
|
149
|
+
|
|
150
|
+
return sorted(indices)
|
|
@@ -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
|