GoshawkVortexAI commited on
Commit
618f347
Β·
verified Β·
1 Parent(s): 78e64c8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -58
app.py CHANGED
@@ -1,61 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import feedparser
 
 
2
  from transformers import pipeline
3
- import re
4
 
5
- # DistilBERT modelini yΓΌkle (CPU ΓΌzerinde)
6
- model = pipeline("sentiment-analysis", model="distilbert-base-uncased", device=-1)
7
-
8
- # Bitcoin haberleri iΓ§in RSS feed URL'si
9
- rss_url = "https://news.google.com/rss/search?q=Bitcoin&hl=en-US&gl=US&ceid=US:en"
10
-
11
- # RSS feed'i al
12
- feed = feedparser.parse(rss_url)
13
-
14
- # Toplam skor ve haber sayΔ±sΔ±
15
- total_score = 0
16
- count = 0
17
-
18
- # Γ–n işleme fonksiyonu
19
- def preprocess_title(title):
20
- # % -> percent, $ -> USD
21
- title = title.replace("%", " percent").replace("$", " USD")
22
- # Gereksiz sembolleri temizle
23
- title = re.sub(r"[^a-zA-Z0-9\s]", "", title)
24
- return title
25
-
26
- # Her başlığı analiz et
27
- for entry in feed.entries:
28
- raw_title = entry.title
29
- title = preprocess_title(raw_title)
30
-
31
- sentiment = model(title)[0]
32
-
33
- label = sentiment['label'].upper()
34
- score = sentiment['score'] # modelin gΓΌven skorunu kullan
35
-
36
- # Ağırlıklı puanlama
37
- if label == "POSITIVE":
38
- total_score += score
39
- elif label == "NEGATIVE":
40
- total_score -= score
41
- else: # NEUTRAL
42
- total_score += 0
43
-
44
- count += 1
45
-
46
- # Ortalama skor
47
- average_score = total_score / count if count > 0 else 0
48
-
49
- # Long / Short / Neutral kararΔ±
50
- threshold = 0.05 # kΓΌΓ§ΓΌk dalgalanmalarΔ± gΓΆrmezden gel
51
- if average_score > threshold:
52
- signal = "LONG 🟒"
53
- elif average_score < -threshold:
54
- signal = "SHORT πŸ”΄"
55
- else:
56
- signal = "NEUTRAL βšͺ️"
57
-
58
- # SonuΓ§larΔ± yazdΔ±r
59
- print(f"Toplam Başlık Sayısı: {count}")
60
- print(f"Ortalama Duygu Skoru: {average_score:.2f}")
61
- print(f"Tahmini Piyasa Sinyali: {signal}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Bitcoin Market Signal Engine β€” Hugging Face Spaces
3
+ ====================================================
4
+ Fetches Bitcoin headlines from Google News RSS, runs DistilBERT sentiment
5
+ analysis, computes a confidence-weighted market signal, and returns JSON.
6
+
7
+ Requirements (see requirements.txt):
8
+ transformers, torch, gradio, feedparser, requests
9
+ """
10
+
11
+ import re
12
+ import json
13
  import feedparser
14
+ import gradio as gr
15
+ from datetime import datetime, timezone
16
  from transformers import pipeline
 
17
 
18
+ # ── Model (loaded once at startup) ──────────────────────────────────────────
19
+ print("Loading DistilBERT sentiment model...")
20
+ sentiment_pipe = pipeline(
21
+ "sentiment-analysis",
22
+ model="distilbert-base-uncased-finetuned-sst-2-english",
23
+ device=-1, # CPU; set to 0 if GPU available
24
+ truncation=True,
25
+ max_length=512,
26
+ )
27
+ print("Model ready.")
28
+
29
+ RSS_URL = (
30
+ "https://news.google.com/rss/search"
31
+ "?q=Bitcoin&hl=en-US&gl=US&ceid=US:en"
32
+ )
33
+
34
+
35
+ # ── Helpers ──────────────────────────────────────────────────────────────────
36
+
37
+ def preprocess(text: str) -> str:
38
+ """Normalise a headline for the model."""
39
+ text = text.replace("%", "percent").replace("$", "USD")
40
+ text = re.sub(r"[^a-zA-Z0-9 ]", " ", text)
41
+ text = re.sub(r"\s+", " ", text).strip().lower()
42
+ return text
43
+
44
+
45
+ def fetch_headlines(max_items: int = 30) -> list[dict]:
46
+ """Pull headlines from Google News RSS via feedparser."""
47
+ feed = feedparser.parse(RSS_URL)
48
+ entries = feed.entries[:max_items]
49
+ if not entries:
50
+ raise ValueError("RSS feed returned no items. Try again shortly.")
51
+ return [
52
+ {
53
+ "raw": e.get("title", ""),
54
+ "link": e.get("link", ""),
55
+ "published": e.get("published", ""),
56
+ }
57
+ for e in entries
58
+ ]
59
+
60
+
61
+ def classify(headlines: list[dict]) -> list[dict]:
62
+ """
63
+ Run DistilBERT on preprocessed headlines.
64
+ Maps POSITIVE β†’ +score, NEGATIVE β†’ -score, NEUTRAL β†’ 0.
65
+ DistilBERT SST-2 only emits POSITIVE/NEGATIVE; we keep that faithful.
66
+ """
67
+ cleaned = [preprocess(h["raw"]) for h in headlines]
68
+ raw_preds = sentiment_pipe(cleaned, batch_size=16)
69
+
70
+ results = []
71
+ for h, pred in zip(headlines, raw_preds):
72
+ label = pred["label"] # "POSITIVE" or "NEGATIVE"
73
+ score = float(pred["score"]) # confidence ∈ (0.5, 1.0]
74
+ numeric = score if label == "POSITIVE" else -score
75
+ results.append(
76
+ {
77
+ **h,
78
+ "clean": preprocess(h["raw"]),
79
+ "label": label,
80
+ "confidence": round(score, 6),
81
+ "numeric_score": round(numeric, 6),
82
+ }
83
+ )
84
+ return results
85
+
86
+
87
+ def compute_signal(scored: list[dict]) -> dict:
88
+ """Weighted average β†’ LONG / SHORT / NEUTRAL signal."""
89
+ avg = sum(s["numeric_score"] for s in scored) / len(scored)
90
+ signal = "LONG" if avg > 0.05 else "SHORT" if avg < -0.05 else "NEUTRAL"
91
+
92
+ by_score = sorted(scored, key=lambda x: x["numeric_score"], reverse=True)
93
+ top_pos = [s["raw"] for s in by_score if s["numeric_score"] > 0][:5]
94
+ top_neg = [s["raw"] for s in reversed(by_score) if s["numeric_score"] < 0][:5]
95
+
96
+ label_counts = {"POSITIVE": 0, "NEGATIVE": 0}
97
+ for s in scored:
98
+ label_counts[s["label"]] = label_counts.get(s["label"], 0) + 1
99
+
100
+ return {
101
+ "timestamp": datetime.now(timezone.utc).isoformat(),
102
+ "headline_count": len(scored),
103
+ "label_distribution": label_counts,
104
+ "average_score": round(avg, 6),
105
+ "market_signal": signal,
106
+ "top_positive_headlines": top_pos,
107
+ "top_negative_headlines": top_neg,
108
+ }
109
+
110
+
111
+ # ── Gradio handler ───────────────────────────────────────────────────────────
112
+
113
+ def run_analysis(max_headlines: int, show_detail: bool):
114
+ """Main Gradio callback."""
115
+ try:
116
+ # 1. Fetch
117
+ headlines = fetch_headlines(int(max_headlines))
118
+
119
+ # 2. Classify
120
+ scored = classify(headlines)
121
+
122
+ # 3. Signal
123
+ result = compute_signal(scored)
124
+
125
+ # 4. Build outputs
126
+ signal_md = (
127
+ f"## Signal: **{result['market_signal']}**\n\n"
128
+ f"- **Average score:** `{result['average_score']}`\n"
129
+ f"- **Headlines analysed:** {result['headline_count']}\n"
130
+ f"- **Positive:** {result['label_distribution'].get('POSITIVE', 0)} "
131
+ f"**Negative:** {result['label_distribution'].get('NEGATIVE', 0)}\n"
132
+ f"- **Timestamp:** {result['timestamp']}"
133
+ )
134
+
135
+ # Core JSON (spec-compliant)
136
+ core_json = {
137
+ k: result[k]
138
+ for k in (
139
+ "timestamp",
140
+ "headline_count",
141
+ "average_score",
142
+ "market_signal",
143
+ "top_positive_headlines",
144
+ "top_negative_headlines",
145
+ )
146
+ }
147
+ json_out = json.dumps(core_json, indent=2)
148
+
149
+ # Detail table
150
+ if show_detail:
151
+ detail_rows = [
152
+ [
153
+ i + 1,
154
+ s["label"],
155
+ f"{s['numeric_score']:+.4f}",
156
+ s["raw"][:120],
157
+ ]
158
+ for i, s in enumerate(scored)
159
+ ]
160
+ else:
161
+ detail_rows = []
162
+
163
+ return signal_md, json_out, detail_rows, ""
164
+
165
+ except Exception as exc:
166
+ return "", "", [], f"❌ Error: {exc}"
167
+
168
+
169
+ # ── UI ───────────────────────────────────────────────────────────────────────
170
+
171
+ with gr.Blocks(
172
+ title="Bitcoin Market Signal Engine",
173
+ theme=gr.themes.Monochrome(),
174
+ css="""
175
+ #signal-md { font-size: 1.15rem; }
176
+ #json-box textarea { font-family: monospace; font-size: 0.82rem; }
177
+ footer { display: none !important; }
178
+ """,
179
+ ) as demo:
180
+
181
+ gr.Markdown(
182
+ """
183
+ # β‚Ώ Bitcoin Market Signal Engine
184
+ Fetches live Bitcoin headlines β†’ DistilBERT sentiment β†’ confidence-weighted signal.
185
+ """
186
+ )
187
+
188
+ with gr.Row():
189
+ max_h = gr.Slider(5, 30, value=20, step=1, label="Max headlines to fetch")
190
+ show_d = gr.Checkbox(value=True, label="Show per-headline detail table")
191
+ run_btn = gr.Button("β–Ά Run Analysis", variant="primary")
192
+
193
+ with gr.Row():
194
+ signal_out = gr.Markdown(elem_id="signal-md", label="Market Signal")
195
+
196
+ json_out = gr.Code(
197
+ label="JSON Output (spec-compliant)",
198
+ language="json",
199
+ elem_id="json-box",
200
+ )
201
+
202
+ detail_tbl = gr.Dataframe(
203
+ headers=["#", "Label", "Score", "Headline"],
204
+ label="Headline Sentiment Detail",
205
+ wrap=True,
206
+ )
207
+
208
+ error_box = gr.Textbox(label="Errors", visible=True, interactive=False)
209
+
210
+ run_btn.click(
211
+ fn=run_analysis,
212
+ inputs=[max_h, show_d],
213
+ outputs=[signal_out, json_out, detail_tbl, error_box],
214
+ )
215
+
216
+ gr.Markdown(
217
+ """
218
+ ---
219
+ **Pipeline:** RSS fetch β†’ preprocess (% β†’ percent, $ β†’ USD, strip special chars, lowercase)
220
+ β†’ `distilbert-base-uncased-finetuned-sst-2-english` β†’ confidence-weighted average
221
+ β†’ LONG (avg > 0.05) / SHORT (avg < -0.05) / NEUTRAL
222
+ """
223
+ )
224
+
225
+ if __name__ == "__main__":
226
+ demo.launch()