eshan6704 commited on
Commit
a2ce407
·
verified ·
1 Parent(s): bd67364

Create screener.py

Browse files
Files changed (1) hide show
  1. app/screener.py +159 -0
app/screener.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import pandas as pd
3
+ from bs4 import BeautifulSoup
4
+ from typing import List, Tuple
5
+
6
+ from . import persist
7
+
8
+
9
+ # ===============================
10
+ # Screener name → URL mapping
11
+ # ===============================
12
+ SCREENER_MAP = {
13
+ "top_gainers": "https://www.screener.in/screens/7054/top-gainers/",
14
+ "top_losers": "https://www.screener.in/screens/7055/top-losers/",
15
+ "high_volume": "https://www.screener.in/screens/3352/high-volume-stocks/",
16
+ "breakout_stocks": "https://www.screener.in/screens/1849/breakout-stocks/",
17
+ "fifty_two_wk_high": "https://www.screener.in/screens/3588/52-week-high/",
18
+ "fifty_two_wk_low": "https://www.screener.in/screens/3589/52-week-low/",
19
+ "new_highs": "https://www.screener.in/screens/4921/new-highs/",
20
+ "new_lows": "https://www.screener.in/screens/4922/new-lows/",
21
+ "rsi_oversold": "https://www.screener.in/screens/1751/rsi-oversold-stocks/",
22
+ "rsi_overbought": "https://www.screener.in/screens/1750/rsi-overbought-stocks/",
23
+ "price_above_sma": "https://www.screener.in/screens/1764/price-above-sma/",
24
+ "large_cap": "https://www.screener.in/screens/3360/large-cap-stocks/",
25
+ "mid_cap": "https://www.screener.in/screens/3361/mid-cap-stocks/",
26
+ "small_cap": "https://www.screener.in/screens/3362/small-cap-stocks/",
27
+ }
28
+
29
+
30
+ # ===============================
31
+ # Public API
32
+ # ===============================
33
+ def fetch_screener(screen_name: str) -> str:
34
+ """
35
+ Returns a fully styled HTML table for a given screener name.
36
+ Uses disk persistence (HTML primary, CSV secondary).
37
+ """
38
+
39
+ if screen_name not in SCREENER_MAP:
40
+ return _error_html(f"Invalid screener: {screen_name}")
41
+
42
+ cache_name = f"SCREENER_{screen_name.upper()}"
43
+
44
+ # 1️⃣ Cache hit
45
+ if persist.exists(cache_name, "html"):
46
+ return persist.load(cache_name, "html")
47
+
48
+ # 2️⃣ Fetch live
49
+ headers, rows = _fetch_table(SCREENER_MAP[screen_name])
50
+
51
+ if not headers or not rows:
52
+ return _error_html("No data available")
53
+
54
+ # 3️⃣ Build outputs
55
+ html = _build_html(headers, rows)
56
+ csv_df = pd.DataFrame(rows, columns=headers)
57
+
58
+ # 4️⃣ Persist
59
+ persist.save(cache_name, html, "html")
60
+ persist.save(cache_name, csv_df, "csv")
61
+
62
+ return html
63
+
64
+
65
+ # ===============================
66
+ # Internal helpers
67
+ # ===============================
68
+ def _fetch_table(url: str) -> Tuple[List[str], List[List[str]]]:
69
+ r = requests.get(
70
+ url,
71
+ headers={"User-Agent": "Mozilla/5.0"},
72
+ timeout=15
73
+ )
74
+ r.raise_for_status()
75
+
76
+ soup = BeautifulSoup(r.text, "html.parser")
77
+ table = soup.find("table")
78
+ if not table:
79
+ return [], []
80
+
81
+ thead = table.find("thead")
82
+ header_row = thead.find("tr") if thead else table.find("tr")
83
+ headers = [th.get_text(strip=True) for th in header_row.find_all("th")]
84
+
85
+ rows = []
86
+ for tr in table.find_all("tr")[1:]:
87
+ cells = tr.find_all("td")
88
+ if cells:
89
+ rows.append([td.get_text(strip=True) for td in cells])
90
+
91
+ return headers, rows
92
+
93
+
94
+ def _build_html(headers: List[str], rows: List[List[str]]) -> str:
95
+ style = """
96
+ <style>
97
+ .screener-wrap {
98
+ width: 100%;
99
+ overflow-x: auto;
100
+ font-family: Arial, sans-serif;
101
+ }
102
+ table.screener {
103
+ border-collapse: collapse;
104
+ width: 100%;
105
+ min-width: 900px;
106
+ font-size: 13px;
107
+ }
108
+ table.screener th {
109
+ position: sticky;
110
+ top: 0;
111
+ background: #1e293b;
112
+ color: #ffffff;
113
+ padding: 8px;
114
+ border: 1px solid #334155;
115
+ white-space: nowrap;
116
+ }
117
+ table.screener td {
118
+ padding: 6px 8px;
119
+ border: 1px solid #e5e7eb;
120
+ white-space: nowrap;
121
+ }
122
+ table.screener tr:nth-child(even) {
123
+ background: #f8fafc;
124
+ }
125
+ table.screener tr:hover {
126
+ background: #e0f2fe;
127
+ }
128
+ </style>
129
+ """
130
+
131
+ html = [style, "<div class='screener-wrap'>", "<table class='screener'>"]
132
+
133
+ html.append("<tr>")
134
+ for h in headers:
135
+ html.append(f"<th>{h}</th>")
136
+ html.append("</tr>")
137
+
138
+ for row in rows:
139
+ html.append("<tr>")
140
+ for cell in row:
141
+ html.append(f"<td>{cell}</td>")
142
+ html.append("</tr>")
143
+
144
+ html.append("</table></div>")
145
+ return "".join(html)
146
+
147
+
148
+ def _error_html(msg: str) -> str:
149
+ return f"""
150
+ <div style="
151
+ color:#b91c1c;
152
+ background:#fee2e2;
153
+ padding:12px;
154
+ border-radius:6px;
155
+ font-family:Arial;
156
+ ">
157
+ ❌ {msg}
158
+ </div>
159
+ """