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/screener.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Stock Screener for BIST - yfinance-like API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from borsapy._providers.isyatirim_screener import get_screener_provider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Screener:
|
|
11
|
+
"""
|
|
12
|
+
A yfinance-like interface for BIST stock screening.
|
|
13
|
+
|
|
14
|
+
Data source: İş Yatırım
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
>>> import borsapy as bp
|
|
18
|
+
>>> screener = bp.Screener()
|
|
19
|
+
>>> screener.add_filter("market_cap", min=1000) # Min $1B market cap
|
|
20
|
+
>>> screener.add_filter("dividend_yield", min=3) # Min 3% dividend yield
|
|
21
|
+
>>> results = screener.run()
|
|
22
|
+
symbol name market_cap dividend_yield
|
|
23
|
+
0 THYAO Türk Hava Yolları 5234.5 4.2
|
|
24
|
+
1 GARAN Garanti Bankası 8123.4 5.1
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
>>> # Using templates
|
|
28
|
+
>>> results = bp.screen_stocks(template="high_dividend")
|
|
29
|
+
|
|
30
|
+
>>> # Direct filtering
|
|
31
|
+
>>> results = bp.screen_stocks(market_cap_min=1000, pe_max=15)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Available templates
|
|
35
|
+
TEMPLATES = [
|
|
36
|
+
"small_cap",
|
|
37
|
+
"mid_cap",
|
|
38
|
+
"large_cap",
|
|
39
|
+
"high_dividend",
|
|
40
|
+
"high_upside",
|
|
41
|
+
"low_upside",
|
|
42
|
+
"high_volume",
|
|
43
|
+
"low_volume",
|
|
44
|
+
"buy_recommendation",
|
|
45
|
+
"sell_recommendation",
|
|
46
|
+
"high_net_margin",
|
|
47
|
+
"high_return",
|
|
48
|
+
"low_pe",
|
|
49
|
+
"high_roe",
|
|
50
|
+
"high_foreign_ownership",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
"""Initialize Screener."""
|
|
55
|
+
self._provider = get_screener_provider()
|
|
56
|
+
self._filters: list[tuple[str, str, str, str]] = []
|
|
57
|
+
self._sector: str | None = None
|
|
58
|
+
self._index: str | None = None
|
|
59
|
+
self._recommendation: str | None = None
|
|
60
|
+
|
|
61
|
+
# Default min/max values for criteria when only one bound is specified
|
|
62
|
+
# API requires both min and max - these are sensible defaults
|
|
63
|
+
CRITERIA_DEFAULTS = {
|
|
64
|
+
"price": {"min": 0, "max": 100000},
|
|
65
|
+
"market_cap": {"min": 0, "max": 5000000}, # TL millions
|
|
66
|
+
"market_cap_usd": {"min": 0, "max": 100000}, # USD millions
|
|
67
|
+
"pe": {"min": -1000, "max": 10000},
|
|
68
|
+
"pb": {"min": -100, "max": 1000},
|
|
69
|
+
"ev_ebitda": {"min": -100, "max": 1000},
|
|
70
|
+
"ev_sales": {"min": -100, "max": 1000},
|
|
71
|
+
"dividend_yield": {"min": 0, "max": 100},
|
|
72
|
+
"dividend_yield_2025": {"min": 0, "max": 100},
|
|
73
|
+
"roe": {"min": -200, "max": 500},
|
|
74
|
+
"roa": {"min": -200, "max": 500},
|
|
75
|
+
"net_margin": {"min": -200, "max": 500},
|
|
76
|
+
"ebitda_margin": {"min": -200, "max": 500},
|
|
77
|
+
"upside_potential": {"min": -100, "max": 500},
|
|
78
|
+
"foreign_ratio": {"min": 0, "max": 100},
|
|
79
|
+
"float_ratio": {"min": 0, "max": 100},
|
|
80
|
+
"return_1w": {"min": -100, "max": 100},
|
|
81
|
+
"return_1m": {"min": -100, "max": 200},
|
|
82
|
+
"return_1y": {"min": -100, "max": 1000},
|
|
83
|
+
"return_ytd": {"min": -100, "max": 1000},
|
|
84
|
+
"volume_3m": {"min": 0, "max": 1000},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
def add_filter(
|
|
88
|
+
self,
|
|
89
|
+
criteria: str,
|
|
90
|
+
min: float | None = None,
|
|
91
|
+
max: float | None = None,
|
|
92
|
+
required: bool = False,
|
|
93
|
+
) -> "Screener":
|
|
94
|
+
"""
|
|
95
|
+
Add a filter criterion.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
criteria: Criteria name (market_cap, pe, dividend_yield, etc.).
|
|
99
|
+
min: Minimum value.
|
|
100
|
+
max: Maximum value.
|
|
101
|
+
required: Whether this filter is required.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Self for method chaining.
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
>>> screener = Screener()
|
|
108
|
+
>>> screener.add_filter("market_cap", min=1000)
|
|
109
|
+
>>> screener.add_filter("pe", max=15)
|
|
110
|
+
"""
|
|
111
|
+
# Map criteria name to ID
|
|
112
|
+
criteria_map = self._provider.CRITERIA_MAP
|
|
113
|
+
criteria_id = criteria_map.get(criteria.lower(), criteria)
|
|
114
|
+
|
|
115
|
+
# Get default bounds for this criteria
|
|
116
|
+
defaults = self.CRITERIA_DEFAULTS.get(criteria.lower(), {"min": -999999, "max": 999999})
|
|
117
|
+
|
|
118
|
+
# API requires both min and max - use defaults when only one is provided
|
|
119
|
+
if min is None and max is not None:
|
|
120
|
+
min = defaults["min"]
|
|
121
|
+
elif max is None and min is not None:
|
|
122
|
+
max = defaults["max"]
|
|
123
|
+
|
|
124
|
+
min_str = str(min) if min is not None else ""
|
|
125
|
+
max_str = str(max) if max is not None else ""
|
|
126
|
+
required_str = "True" if required else "False"
|
|
127
|
+
|
|
128
|
+
self._filters.append((criteria_id, min_str, max_str, required_str))
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def set_sector(self, sector: str) -> "Screener":
|
|
132
|
+
"""
|
|
133
|
+
Set sector filter.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
sector: Sector name (e.g., "Bankacılık") or ID (e.g., "0001").
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Self for method chaining.
|
|
140
|
+
"""
|
|
141
|
+
# Convert sector name to ID if needed
|
|
142
|
+
if sector and not sector.startswith("0"):
|
|
143
|
+
sectors_data = self._provider.get_sectors()
|
|
144
|
+
for s in sectors_data:
|
|
145
|
+
if s.get("name", "").lower() == sector.lower():
|
|
146
|
+
sector = s.get("id", sector)
|
|
147
|
+
break
|
|
148
|
+
self._sector = sector
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def set_index(self, index: str) -> "Screener":
|
|
152
|
+
"""
|
|
153
|
+
Set index filter.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
index: Index name (e.g., "BIST 30", "BIST 100").
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Self for method chaining.
|
|
160
|
+
"""
|
|
161
|
+
# Note: Index filtering may have limited support in the API
|
|
162
|
+
self._index = index
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def set_recommendation(self, recommendation: str) -> "Screener":
|
|
166
|
+
"""
|
|
167
|
+
Set recommendation filter.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
recommendation: Recommendation type ("AL", "SAT", "TUT").
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Self for method chaining.
|
|
174
|
+
"""
|
|
175
|
+
self._recommendation = recommendation.upper()
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
def clear(self) -> "Screener":
|
|
179
|
+
"""
|
|
180
|
+
Clear all filters.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Self for method chaining.
|
|
184
|
+
"""
|
|
185
|
+
self._filters = []
|
|
186
|
+
self._sector = None
|
|
187
|
+
self._index = None
|
|
188
|
+
self._recommendation = None
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def run(self, template: str | None = None) -> pd.DataFrame:
|
|
192
|
+
"""
|
|
193
|
+
Run the screener and return results.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
template: Optional pre-defined template to use.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
DataFrame with matching stocks.
|
|
200
|
+
"""
|
|
201
|
+
results = self._provider.screen(
|
|
202
|
+
criterias=self._filters if self._filters else None,
|
|
203
|
+
sector=self._sector,
|
|
204
|
+
index=self._index,
|
|
205
|
+
recommendation=self._recommendation,
|
|
206
|
+
template=template,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if not results:
|
|
210
|
+
return pd.DataFrame(columns=["symbol", "name"])
|
|
211
|
+
|
|
212
|
+
return pd.DataFrame(results)
|
|
213
|
+
|
|
214
|
+
def __repr__(self) -> str:
|
|
215
|
+
return f"Screener(filters={len(self._filters)}, sector={self._sector}, index={self._index})"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def screen_stocks(
|
|
219
|
+
template: str | None = None,
|
|
220
|
+
sector: str | None = None,
|
|
221
|
+
index: str | None = None,
|
|
222
|
+
recommendation: str | None = None,
|
|
223
|
+
# Common filters as direct parameters
|
|
224
|
+
market_cap_min: float | None = None,
|
|
225
|
+
market_cap_max: float | None = None,
|
|
226
|
+
pe_min: float | None = None,
|
|
227
|
+
pe_max: float | None = None,
|
|
228
|
+
pb_min: float | None = None,
|
|
229
|
+
pb_max: float | None = None,
|
|
230
|
+
dividend_yield_min: float | None = None,
|
|
231
|
+
dividend_yield_max: float | None = None,
|
|
232
|
+
upside_potential_min: float | None = None,
|
|
233
|
+
upside_potential_max: float | None = None,
|
|
234
|
+
net_margin_min: float | None = None,
|
|
235
|
+
net_margin_max: float | None = None,
|
|
236
|
+
roe_min: float | None = None,
|
|
237
|
+
roe_max: float | None = None,
|
|
238
|
+
) -> pd.DataFrame:
|
|
239
|
+
"""
|
|
240
|
+
Screen BIST stocks based on criteria (convenience function).
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
template: Pre-defined template name:
|
|
244
|
+
- "small_cap": Market cap < $1B
|
|
245
|
+
- "mid_cap": Market cap $1B-$5B
|
|
246
|
+
- "large_cap": Market cap > $5B
|
|
247
|
+
- "high_dividend": Dividend yield > 2%
|
|
248
|
+
- "high_upside": Positive upside potential
|
|
249
|
+
- "buy_recommendation": BUY recommendations
|
|
250
|
+
- "sell_recommendation": SELL recommendations
|
|
251
|
+
- "high_net_margin": Net margin > 10%
|
|
252
|
+
- "high_return": Positive weekly return
|
|
253
|
+
sector: Sector filter (e.g., "Bankacılık").
|
|
254
|
+
index: Index filter (e.g., "BIST30").
|
|
255
|
+
recommendation: "AL", "SAT", or "TUT".
|
|
256
|
+
market_cap_min/max: Market cap in million USD.
|
|
257
|
+
pe_min/max: P/E ratio.
|
|
258
|
+
pb_min/max: P/B ratio.
|
|
259
|
+
dividend_yield_min/max: Dividend yield (%).
|
|
260
|
+
upside_potential_min/max: Upside potential (%).
|
|
261
|
+
net_margin_min/max: Net margin (%).
|
|
262
|
+
roe_min/max: Return on equity (%).
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
DataFrame with matching stocks.
|
|
266
|
+
|
|
267
|
+
Examples:
|
|
268
|
+
>>> import borsapy as bp
|
|
269
|
+
|
|
270
|
+
>>> # Using template
|
|
271
|
+
>>> bp.screen_stocks(template="high_dividend")
|
|
272
|
+
|
|
273
|
+
>>> # Custom filters
|
|
274
|
+
>>> bp.screen_stocks(market_cap_min=1000, pe_max=15)
|
|
275
|
+
|
|
276
|
+
>>> # Combined
|
|
277
|
+
>>> bp.screen_stocks(
|
|
278
|
+
... sector="Bankacılık",
|
|
279
|
+
... dividend_yield_min=3,
|
|
280
|
+
... pe_max=10
|
|
281
|
+
... )
|
|
282
|
+
"""
|
|
283
|
+
screener = Screener()
|
|
284
|
+
|
|
285
|
+
# Set sector/index/recommendation
|
|
286
|
+
if sector:
|
|
287
|
+
screener.set_sector(sector)
|
|
288
|
+
if index:
|
|
289
|
+
screener.set_index(index)
|
|
290
|
+
if recommendation:
|
|
291
|
+
screener.set_recommendation(recommendation)
|
|
292
|
+
|
|
293
|
+
# Add filters
|
|
294
|
+
if market_cap_min is not None or market_cap_max is not None:
|
|
295
|
+
screener.add_filter("market_cap", min=market_cap_min, max=market_cap_max)
|
|
296
|
+
|
|
297
|
+
if pe_min is not None or pe_max is not None:
|
|
298
|
+
screener.add_filter("pe", min=pe_min, max=pe_max)
|
|
299
|
+
|
|
300
|
+
if pb_min is not None or pb_max is not None:
|
|
301
|
+
screener.add_filter("pb", min=pb_min, max=pb_max)
|
|
302
|
+
|
|
303
|
+
if dividend_yield_min is not None or dividend_yield_max is not None:
|
|
304
|
+
screener.add_filter("dividend_yield", min=dividend_yield_min, max=dividend_yield_max)
|
|
305
|
+
|
|
306
|
+
if upside_potential_min is not None or upside_potential_max is not None:
|
|
307
|
+
screener.add_filter("upside_potential", min=upside_potential_min, max=upside_potential_max)
|
|
308
|
+
|
|
309
|
+
if net_margin_min is not None or net_margin_max is not None:
|
|
310
|
+
screener.add_filter("net_margin", min=net_margin_min, max=net_margin_max)
|
|
311
|
+
|
|
312
|
+
if roe_min is not None or roe_max is not None:
|
|
313
|
+
screener.add_filter("roe", min=roe_min, max=roe_max)
|
|
314
|
+
|
|
315
|
+
return screener.run(template=template)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def screener_criteria() -> list[dict[str, Any]]:
|
|
319
|
+
"""
|
|
320
|
+
Get list of available screening criteria.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
List of criteria with id, name, min, max values.
|
|
324
|
+
|
|
325
|
+
Examples:
|
|
326
|
+
>>> import borsapy as bp
|
|
327
|
+
>>> bp.screener_criteria()
|
|
328
|
+
[{'id': '7', 'name': 'Kapanış (TL)', 'min': '1.1', 'max': '14087.5'}, ...]
|
|
329
|
+
"""
|
|
330
|
+
provider = get_screener_provider()
|
|
331
|
+
return provider.get_criteria()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def sectors() -> list[str]:
|
|
335
|
+
"""
|
|
336
|
+
Get list of available sectors for screening.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
List of sector names.
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
>>> import borsapy as bp
|
|
343
|
+
>>> bp.sectors()
|
|
344
|
+
['Bankacılık', 'Holding', 'Enerji', ...]
|
|
345
|
+
"""
|
|
346
|
+
provider = get_screener_provider()
|
|
347
|
+
data = provider.get_sectors()
|
|
348
|
+
return [item["name"] for item in data if item.get("name")]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def stock_indices() -> list[str]:
|
|
352
|
+
"""
|
|
353
|
+
Get list of available indices for screening.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of index names.
|
|
357
|
+
|
|
358
|
+
Examples:
|
|
359
|
+
>>> import borsapy as bp
|
|
360
|
+
>>> bp.stock_indices()
|
|
361
|
+
['BIST30', 'BIST100', 'BIST BANKA', ...]
|
|
362
|
+
"""
|
|
363
|
+
provider = get_screener_provider()
|
|
364
|
+
data = provider.get_indices()
|
|
365
|
+
return [item["name"] for item in data if item.get("name")]
|