GoshawkVortexAI commited on
Commit
deecc37
·
verified ·
1 Parent(s): 19a3b76

Delete news_feed.py

Browse files
Files changed (1) hide show
  1. news_feed.py +0 -347
news_feed.py DELETED
@@ -1,347 +0,0 @@
1
- """news_feed.py — RSS haber taraması + SVG grafikler (ek bağımlılık yok).
2
-
3
- app.py'den: build_news_panel(classify, detect_assets, asset, timeframe)
4
- classify(text) -> (label: "Bullish"|"Neutral"|"Bearish", score: -1..1)
5
- """
6
- import re
7
- import time
8
- import html
9
- import hashlib
10
- import urllib.request
11
- import xml.etree.ElementTree as ET
12
- from concurrent.futures import ThreadPoolExecutor
13
- from datetime import datetime, timezone
14
- from email.utils import parsedate_to_datetime
15
-
16
- FEEDS = {
17
- "CoinDesk": "https://www.coindesk.com/arc/outboundfeeds/rss/",
18
- "Cointelegraph": "https://cointelegraph.com/rss",
19
- "Decrypt": "https://decrypt.co/feed",
20
- "Bitcoin Magazine": "https://bitcoinmagazine.com/.rss/full/",
21
- "The Block": "https://www.theblock.co/rss.xml",
22
- }
23
-
24
- # regex -> ağırlık (önem puanı)
25
- KEYWORDS = {
26
- r"\betf": 3, r"\bsec\b": 3, r"\bhack": 3, r"\bexploit": 3, r"\bliquidat": 3,
27
- r"\bbankrupt": 3, r"\bfed\b": 3, r"\brate (cut|hike)": 3, r"\bcpi\b": 2,
28
- r"\bhalving": 3, r"\bcrash": 3, r"\bplunge": 3, r"\bsurge": 2, r"\brall(y|ies)": 2,
29
- r"all-time high|\bath\b": 3, r"\blawsuit|\bsued?\b": 2, r"\bban(s|ned)?\b": 2,
30
- r"\bapprov": 2, r"\bregulat": 2, r"\bwhale": 2, r"\binflow|\boutflow": 2,
31
- r"\bblackrock": 2, r"\bdelist": 2, r"\bstablecoin": 1, r"\btether|\busdt": 1,
32
- r"\bbinance": 1, r"\bbreakout|\bbreakdown": 2, r"\bsettle|\bfine[sd]?\b": 1,
33
- }
34
- _KW = [(re.compile(k), w) for k, w in KEYWORDS.items()]
35
-
36
- TIMEFRAMES = {"1G": ("2d", "15m"), "7G": ("7d", "1h"), "1A": ("1mo", "1d"), "3A": ("3mo", "1d")}
37
- COL = {"Bullish": "#22c55e", "Neutral": "#eab308", "Bearish": "#ef4444"}
38
-
39
- _news_cache = {"t": 0.0, "items": [], "ok": 0}
40
- _cls_cache = {}
41
- NEWS_TTL = 300
42
-
43
-
44
- # ------------------------------------------------------------------ FETCH
45
- def _strip(s):
46
- return html.unescape(re.sub(r"<[^>]+>", " ", s or "")).strip()
47
-
48
-
49
- def _parse_feed(xml_bytes, source):
50
- root = ET.fromstring(xml_bytes)
51
- atom = "{http://www.w3.org/2005/Atom}"
52
- out = []
53
- for it in root.iter():
54
- tag = it.tag.replace(atom, "")
55
- if tag not in ("item", "entry"):
56
- continue
57
-
58
- def g(name):
59
- el = it.find(name)
60
- if el is None:
61
- el = it.find(atom + name)
62
- return el
63
-
64
- title = g("title")
65
- link = g("link")
66
- date = g("pubDate") or g("published") or g("updated")
67
- desc = g("description") or g("summary")
68
- href = None
69
- if link is not None:
70
- href = link.get("href") or (link.text or "").strip()
71
- ts = None
72
- if date is not None and date.text:
73
- try:
74
- ts = parsedate_to_datetime(date.text)
75
- except Exception:
76
- try:
77
- ts = datetime.fromisoformat(date.text.replace("Z", "+00:00"))
78
- except Exception:
79
- ts = None
80
- if ts is not None and ts.tzinfo is None:
81
- ts = ts.replace(tzinfo=timezone.utc)
82
- if title is not None and title.text:
83
- out.append({
84
- "title": _strip(title.text), "link": href or "#", "source": source,
85
- "ts": ts or datetime.now(timezone.utc),
86
- "summary": _strip(desc.text if desc is not None else "")[:300],
87
- })
88
- return out
89
-
90
-
91
- def _fetch_one(item):
92
- name, url = item
93
- try:
94
- req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 GoshawkVortex/1.0"})
95
- with urllib.request.urlopen(req, timeout=8) as r:
96
- return _parse_feed(r.read(), name)
97
- except Exception:
98
- return None
99
-
100
-
101
- def fetch_news(force=False):
102
- if not force and time.time() - _news_cache["t"] < NEWS_TTL and _news_cache["items"]:
103
- return _news_cache["items"], _news_cache["ok"]
104
- with ThreadPoolExecutor(max_workers=len(FEEDS)) as ex:
105
- results = list(ex.map(_fetch_one, FEEDS.items()))
106
- items = [i for r in results if r for i in r]
107
- ok = sum(1 for r in results if r)
108
- if items:
109
- _news_cache.update(t=time.time(), items=items, ok=ok)
110
- return items or _news_cache["items"], ok
111
-
112
-
113
- # ------------------------------------------------------------------ RANK
114
- def _tokens(t):
115
- return {w for w in re.findall(r"[a-z0-9]+", t.lower()) if len(w) > 3}
116
-
117
-
118
- def rank_news(items, limit=12, max_age_h=48):
119
- now = datetime.now(timezone.utc)
120
- items = [i for i in items if (now - i["ts"]).total_seconds() < max_age_h * 3600]
121
- items.sort(key=lambda i: i["ts"], reverse=True)
122
- clusters = []
123
- for it in items:
124
- tk = _tokens(it["title"])
125
- for c in clusters:
126
- if tk and len(tk & c["tk"]) / len(tk | c["tk"]) >= 0.5:
127
- c["srcs"].add(it["source"])
128
- c["n"] += 1
129
- break
130
- else:
131
- clusters.append({"it": it, "tk": tk, "srcs": {it["source"]}, "n": 1})
132
- ranked = []
133
- for c in clusters:
134
- it = c["it"]
135
- text = (it["title"] + " " + it["summary"]).lower()
136
- kw = sum(w for rx, w in _KW if rx.search(text))
137
- age_h = (now - it["ts"]).total_seconds() / 3600
138
- rec = 0.5 ** (age_h / 12)
139
- imp = (kw + 1.5 * (len(c["srcs"]) - 1)) * (0.35 + 0.65 * rec)
140
- if kw == 0 and len(c["srcs"]) == 1:
141
- imp *= 0.3
142
- ranked.append({**it, "srcs": sorted(c["srcs"]), "imp": min(100, imp * 10), "age_h": age_h})
143
- ranked.sort(key=lambda r: r["imp"], reverse=True)
144
- return ranked[:limit]
145
-
146
-
147
- def classify_cached(classify, item):
148
- key = hashlib.sha1(item["title"].encode()).hexdigest()
149
- if key not in _cls_cache:
150
- if len(_cls_cache) > 500:
151
- _cls_cache.clear()
152
- try:
153
- _cls_cache[key] = classify(item["title"] + ". " + item["summary"][:200])
154
- except Exception:
155
- _cls_cache[key] = ("Neutral", 0.0)
156
- return _cls_cache[key]
157
-
158
-
159
- # ------------------------------------------------------------------ PRICE
160
- def price_series(asset, timeframe):
161
- import yfinance as yf # lazy: sadece grafik için
162
- period, interval = TIMEFRAMES.get(timeframe, TIMEFRAMES["1A"])
163
- df = yf.Ticker(f"{asset}-USD").history(period=period, interval=interval)
164
- rows = []
165
- for ts, r in df.iterrows():
166
- if r["Close"] == r["Close"]:
167
- rows.append((ts.to_pydatetime(), float(r["Open"]), float(r["High"]),
168
- float(r["Low"]), float(r["Close"]), float(r.get("Volume", 0) or 0)))
169
- return rows
170
-
171
-
172
- def _fmt(v):
173
- if v >= 1000:
174
- return f"{v:,.0f}"
175
- if v >= 1:
176
- return f"{v:,.2f}"
177
- return f"{v:.5f}"
178
-
179
-
180
- # ------------------------------------------------------------------ SVG
181
- def svg_candles(rows, asset, timeframe, marks=()):
182
- """Mum grafiği + hacim + SMA20 + son fiyat etiketi. marks: [(datetime, renk)] haber işaretleri."""
183
- if len(rows) < 3:
184
- return "<div class='nw-empty'>Fiyat verisi alınamadı.</div>"
185
- W, H, L, R, T = 780, 380, 62, 14, 16
186
- PB, VT, VB = 270, 286, 350
187
- n = len(rows)
188
- lo = min(r[3] for r in rows)
189
- hi = max(r[2] for r in rows)
190
- pad = (hi - lo) * 0.06 or hi * 0.01
191
- lo, hi = lo - pad, hi + pad
192
- vmax = max(r[5] for r in rows) or 1
193
- cw = (W - L - R) / n
194
- px = lambda i: L + cw * (i + 0.5)
195
- py = lambda v: T + (PB - T) * (1 - (v - lo) / (hi - lo))
196
- up = rows[-1][4] >= rows[0][1]
197
- line = "#22c55e" if up else "#ef4444"
198
- o = [f"<svg class='nw-svg' viewBox='0 0 {W} {H}' xmlns='http://www.w3.org/2000/svg'>",
199
- "<defs><linearGradient id='nwa' x1='0' y1='0' x2='0' y2='1'>"
200
- f"<stop offset='0' stop-color='{line}' stop-opacity='.28'/>"
201
- f"<stop offset='1' stop-color='{line}' stop-opacity='0'/></linearGradient></defs>"]
202
- for k in range(6):
203
- v = lo + (hi - lo) * k / 5
204
- y = py(v)
205
- o.append(f"<line x1='{L}' x2='{W-R}' y1='{y:.1f}' y2='{y:.1f}' stroke='#94a3b8' stroke-opacity='.12'/>")
206
- o.append(f"<text x='{L-8}' y='{y+4:.1f}' text-anchor='end' fill='#8295ad' font-size='11'>{_fmt(v)}</text>")
207
- # alan dolgusu (kapanış)
208
- pts = " ".join(f"{px(i):.1f},{py(r[4]):.1f}" for i, r in enumerate(rows))
209
- o.append(f"<polygon points='{px(0):.1f},{PB} {pts} {px(n-1):.1f},{PB}' fill='url(#nwa)'/>")
210
- # hacim + mumlar
211
- for i, (ts, op, h, l, c, v) in enumerate(rows):
212
- col = "#22c55e" if c >= op else "#ef4444"
213
- x = px(i)
214
- bw = max(1.2, cw * 0.62)
215
- vh = (VB - VT) * (v / vmax)
216
- o.append(f"<rect x='{x-bw/2:.1f}' y='{VB-vh:.1f}' width='{bw:.1f}' height='{vh:.1f}' fill='{col}' opacity='.35'/>")
217
- y1, y2 = py(max(op, c)), py(min(op, c))
218
- tip = f"{ts:%d %b %H:%M} O {_fmt(op)} H {_fmt(h)} L {_fmt(l)} C {_fmt(c)}"
219
- o.append(f"<g><title>{tip}</title><line x1='{x:.1f}' x2='{x:.1f}' y1='{py(h):.1f}' y2='{py(l):.1f}' stroke='{col}' stroke-width='1.2'/>"
220
- f"<rect x='{x-bw/2:.1f}' y='{y1:.1f}' width='{bw:.1f}' height='{max(1.2, y2-y1):.1f}' fill='{col}' rx='1'/></g>")
221
- # SMA20
222
- if n >= 20:
223
- sma = [(i, sum(r[4] for r in rows[i-19:i+1]) / 20) for i in range(19, n)]
224
- o.append("<polyline fill='none' stroke='#60a5fa' stroke-width='2' opacity='.9' points='"
225
- + " ".join(f"{px(i):.1f},{py(v):.1f}" for i, v in sma) + "'/>")
226
- # haber işaretleri (zamana en yakın muma)
227
- for mts, mcol, mtitle in marks:
228
- try:
229
- idx = min(range(n), key=lambda i: abs((rows[i][0].replace(tzinfo=None) - mts.replace(tzinfo=None)).total_seconds()))
230
- if abs((rows[idx][0].replace(tzinfo=None) - mts.replace(tzinfo=None)).total_seconds()) > 3 * 86400:
231
- continue
232
- except Exception:
233
- continue
234
- x = px(idx)
235
- o.append(f"<g><title>{html.escape(mtitle)}</title><path d='M{x-6:.1f},{PB+14} L{x+6:.1f},{PB+14} L{x:.1f},{PB+4} Z' fill='{mcol}'/></g>")
236
- # son fiyat etiketi
237
- last = rows[-1][4]
238
- y = py(last)
239
- o.append(f"<line x1='{L}' x2='{W-R}' y1='{y:.1f}' y2='{y:.1f}' stroke='{line}' stroke-dasharray='4 4' opacity='.8'/>")
240
- o.append(f"<rect x='{W-R-70}' y='{y-10:.1f}' width='70' height='20' rx='5' fill='{line}'/>"
241
- f"<text x='{W-R-35}' y='{y+4:.1f}' text-anchor='middle' fill='#fff' font-size='11' font-weight='700'>{_fmt(last)}</text>")
242
- # x ekseni
243
- fmt = "%H:%M" if timeframe == "1G" else "%d %b"
244
- for k in range(5):
245
- i = int((n - 1) * k / 4)
246
- o.append(f"<text x='{px(i):.1f}' y='{H-8}' text-anchor='middle' fill='#8295ad' font-size='11'>{rows[i][0]:{fmt}}</text>")
247
- o.append("</svg>")
248
- chg = (last / rows[0][1] - 1) * 100
249
- head = (f"<div class='nw-head'><b>{asset}/USD</b><span class='nw-price'>{_fmt(last)}</span>"
250
- f"<span style='color:{line}'>{chg:+.2f}%</span><span class='nw-dim'>{timeframe} • mum + hacim + SMA20 • ▲ haber</span></div>")
251
- return head + "".join(o)
252
-
253
-
254
- def svg_donut(counts, net):
255
- tot = sum(counts.values()) or 1
256
- r, C = 52, 2 * 3.14159265 * 52
257
- o = ["<svg class='nw-donut' viewBox='0 0 140 140' xmlns='http://www.w3.org/2000/svg'>",
258
- "<circle cx='70' cy='70' r='52' fill='none' stroke='#172235' stroke-width='16'/>"]
259
- off = 0
260
- for k in ("Bullish", "Neutral", "Bearish"):
261
- ln = C * counts[k] / tot
262
- if ln > 0:
263
- o.append(f"<circle cx='70' cy='70' r='52' fill='none' stroke='{COL[k]}' stroke-width='16' "
264
- f"stroke-dasharray='{ln:.2f} {C-ln:.2f}' stroke-dashoffset='{-off:.2f}' transform='rotate(-90 70 70)'/>")
265
- off += ln
266
- c = "#22c55e" if net > 0.05 else "#ef4444" if net < -0.05 else "#eab308"
267
- o.append(f"<text x='70' y='68' text-anchor='middle' fill='{c}' font-size='24' font-weight='800'>{net:+.2f}</text>"
268
- "<text x='70' y='86' text-anchor='middle' fill='#8295ad' font-size='9' letter-spacing='1'>NET SKOR</text></svg>")
269
- return "".join(o)
270
-
271
-
272
- # ------------------------------------------------------------------ PANEL
273
- NEWS_CSS = """
274
- .nw-wrap { margin-top:12px; color:#dbeafe; }
275
- .nw-card, .nw-news { padding:18px; border-radius:18px; margin-bottom:14px;
276
- background:linear-gradient(180deg, rgba(8,18,34,.9), rgba(5,13,25,.85));
277
- border:1px solid rgba(96,165,250,.18); box-shadow:0 14px 36px rgba(0,0,0,.25); }
278
- .nw-head { display:flex; gap:14px; align-items:baseline; flex-wrap:wrap; margin-bottom:6px; }
279
- .nw-price { font-size:26px; font-weight:800; }
280
- .nw-dim { color:#64748b; font-size:11px; margin-left:auto; }
281
- .nw-svg { width:100%; height:auto; display:block; }
282
- .nw-grid { display:grid; grid-template-columns:160px 1fr; gap:18px; align-items:center; }
283
- .nw-donut { width:150px; height:150px; }
284
- .nw-legend span { display:inline-block; margin:3px 14px 3px 0; font-size:12px; }
285
- .nw-dot { display:inline-block; width:9px; height:9px; border-radius:50%; margin-right:6px; }
286
- .nw-news { border-left:4px solid var(--c); padding:14px 16px; margin-bottom:10px; }
287
- .nw-news a { color:#e2e8f0; font-weight:600; text-decoration:none; }
288
- .nw-news a:hover { color:#60a5fa; }
289
- .nw-meta { color:#7d8ea6; font-size:11px; margin-top:6px; display:flex; gap:10px; flex-wrap:wrap; align-items:center; }
290
- .nw-chip { padding:2px 9px; border-radius:99px; font-weight:700; font-size:11px; background:var(--c); color:#06101f; }
291
- .nw-imp { width:70px; height:6px; border-radius:9px; background:#172235; overflow:hidden; }
292
- .nw-imp i { display:block; height:100%; background:linear-gradient(90deg,#60a5fa,#f97316); }
293
- .nw-empty { color:#8295ad; padding:18px; }
294
- @media (max-width:640px){ .nw-grid { grid-template-columns:1fr; justify-items:center; } }
295
- """
296
-
297
-
298
- def build_news_panel(classify, detect_assets, asset="BTC", timeframe="1A", limit=10):
299
- items, ok = fetch_news()
300
- if not items:
301
- return ("<div class='nw-card nw-empty'>Haber kaynaklarına ulaşılamadı. "
302
- "İnternet/ağ erişimini veya FEEDS adreslerini kontrol et.</div>")
303
- if asset != "ALL":
304
- items = [i for i in items if asset in detect_assets(i["title"] + " " + i["summary"])]
305
- top = rank_news(items, limit=limit)
306
- for t in top:
307
- t["label"], t["score"] = classify_cached(classify, t)
308
-
309
- counts = {"Bullish": 0, "Neutral": 0, "Bearish": 0}
310
- wsum = nsum = 0.0
311
- for t in top:
312
- counts[t["label"] if t["label"] in counts else "Neutral"] += 1
313
- w = 1 + t["imp"] / 25
314
- wsum += w
315
- nsum += w * t["score"]
316
- net = nsum / wsum if wsum else 0.0
317
-
318
- chart = ""
319
- if asset != "ALL":
320
- try:
321
- rows = price_series(asset, timeframe)
322
- marks = [(t["ts"], COL.get(t["label"], "#eab308"), t["title"]) for t in top]
323
- chart = f"<div class='nw-card'>{svg_candles(rows, asset, timeframe, marks)}</div>"
324
- except Exception as e:
325
- chart = f"<div class='nw-card nw-empty'>Fiyat grafiği yüklenemedi: {html.escape(str(e))[:120]}</div>"
326
-
327
- legend = "".join(f"<span><i class='nw-dot' style='background:{COL[k]}'></i>{k} {counts[k]}</span>" for k in COL)
328
- summary = (f"<div class='nw-card nw-grid'>{svg_donut(counts, net)}<div>"
329
- f"<b>Haber duygu özeti</b> <span class='nw-dim'>önem ağırlıklı • {len(top)} haber • {ok}/{len(FEEDS)} kaynak</span>"
330
- f"<div class='nw-legend' style='margin-top:8px'>{legend}</div></div></div>")
331
-
332
- cards = []
333
- for t in top:
334
- c = COL.get(t["label"], "#eab308")
335
- age = f"{t['age_h']*60:.0f} dk" if t["age_h"] < 1 else f"{t['age_h']:.0f} sa"
336
- srcs = ", ".join(t["srcs"])
337
- cards.append(
338
- f"<div class='nw-news' style='--c:{c}'><a href='{html.escape(t['link'])}' target='_blank' rel='noopener'>"
339
- f"{html.escape(t['title'])}</a><div class='nw-meta'><span class='nw-chip'>{t['label']} {t['score']:+.1f}</span>"
340
- f"<span>{html.escape(srcs)}</span><span>{age} önce</span>"
341
- f"<span class='nw-imp' title='Önem {t['imp']:.0f}/100'><i style='width:{t['imp']:.0f}%'></i></span></div></div>")
342
- if not cards:
343
- cards = ["<div class='nw-card nw-empty'>Bu varlık için son 48 saatte önemli haber bulunamadı.</div>"]
344
-
345
- stamp = datetime.now().strftime("%H:%M:%S")
346
- return (f"<div class='nw-wrap'><div class='nw-dim' style='margin:0 0 8px'>Son tarama: {stamp} • "
347
- f"önbellek {NEWS_TTL//60} dk</div>{chart}{summary}{''.join(cards)}</div>")