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/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")]