csjjin2002 commited on
Commit
74ef49c
ยท
verified ยท
1 Parent(s): 6c882df

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +62 -9
  2. app.py +289 -0
  3. requirements.txt +11 -0
README.md CHANGED
@@ -1,12 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Cho Risk Scoring
3
- emoji: ๐Ÿ˜ป
4
- colorFrom: red
5
- colorTo: yellow
6
- sdk: gradio
7
- sdk_version: 5.38.0
8
- app_file: app.py
9
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
1
+ # ๐Ÿง  Cho's Risk Scoring App (Powered by DSPy & Gradio)
2
+
3
+ A real-time risk evaluation tool for retail investors, built with LLM-based Chain-of-Thought (CoT) reasoning.
4
+ This app analyzes stock-related questions and visualizes risk scores across 6 dimensions using natural language inputs.
5
+
6
+ ---
7
+
8
+ ## ๐Ÿ“Œ How to Use
9
+
10
+ 1. Enter a natural language query like:
11
+ ```
12
+ Is Tesla risky these days?
13
+ ์‚ผ์„ฑ์ „์ž๋Š” ์ง€๊ธˆ ๊ณ ํ‰๊ฐ€๋์–ด?
14
+ ```
15
+ 2. Click `Submit`.
16
+ 3. The app will automatically:
17
+ - Detect the stock ticker (US or Korea)
18
+ - Fetch recent Yahoo Finance headlines
19
+ - Run CoT-based risk scoring via OpenAI GPT-4o
20
+ - Display results as:
21
+ - ๐Ÿ“ˆ 1-Month price trend (Plotly)
22
+ - ๐Ÿ“ฐ Headline list (HTML)
23
+ - ๐Ÿ“Š Risk breakdown bar chart (Matplotlib)
24
+ - ๐Ÿง  Detailed reasoning per risk category
25
+
26
+ ---
27
+
28
+ ## ๐Ÿง  Risk Factors Evaluated
29
+
30
+ - Overvaluation
31
+ - Poor Earnings
32
+ - Financial Instability
33
+ - Theme Overheating
34
+ - Recurring Negatives
35
+ - FII Sell-off
36
+
37
  ---
38
+
39
+ ## ๐Ÿ› ๏ธ Tech Stack
40
+
41
+ - `Gradio`: frontend UI
42
+ - `DSPy`: CoT prompting & reasoning
43
+ - `yfinance`: stock price and metadata
44
+ - `Yahoo RSS`: finance news headlines
45
+ - `matplotlib` / `plotly`: data visualization
46
+
47
+ ---
48
+
49
+ ## ๐Ÿ” Environment Variable (required)
50
+
51
+ This app uses OpenAI's GPT-4o via the DSPy framework.
52
+ You **must** define the following secret in Hugging Face:
53
+
54
+ ```
55
+ OPENAI_API_KEY=sk-... โ† your actual OpenAI API key
56
+ ```
57
+
58
+ Add it in your Hugging Face Space โ†’ `Settings` โ†’ `Secrets`.
59
+
60
  ---
61
 
62
+ ## ๐Ÿซ About
63
+
64
+ Developed by HYU researcher Cho
65
+ Based on [DSPy](https://github.com/stanfordnlp/dspy) by Stanford NLP Group
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dspy
2
+ import yfinance as yf
3
+ import os
4
+ import requests
5
+ import re
6
+ import matplotlib.pyplot as plt
7
+ import gradio as gr
8
+ from bs4 import BeautifulSoup
9
+ from datetime import datetime, timedelta, timezone
10
+ import io
11
+ import textwrap
12
+ import plotly.graph_objects as go
13
+ import plotly.io as pio
14
+
15
+ # === ํ™˜๊ฒฝ ์„ค์ • ===
16
+ os.environ["USER_AGENT"] = "Mozilla/5.0"
17
+ original_get = requests.get
18
+ def patched_get(url, *args, **kwargs):
19
+ headers = kwargs.get("headers", {})
20
+ headers.update({"User-Agent": os.environ["USER_AGENT"]})
21
+ kwargs["headers"] = headers
22
+ return original_get(url, *args, **kwargs)
23
+ requests.get = patched_get
24
+
25
+ # === LLM ์„ค์ • ===
26
+ api_key = os.environ.get("OPENAI_API_KEY")
27
+ lm = dspy.LM("openai/gpt-4o", api_key=api_key)
28
+ dspy.configure(lm=lm)
29
+
30
+ # === ํ‹ฐ์ปค ์ถ”์ถœ ์„œ๋ช… ===
31
+ class ExtractTicker(dspy.Signature):
32
+ user_input: str = dspy.InputField()
33
+ suggested_ticker: str = dspy.OutputField()
34
+
35
+ ticker_extractor = dspy.Predict(ExtractTicker)
36
+
37
+ # === ํ•œ๊ตญ ์ข…๋ชฉ ํ‹ฐ์ปค ์ถ”์ถœ ์„œ๋ช… ===
38
+ class ExtractKRStock(dspy.Signature):
39
+ user_input: str = dspy.InputField()
40
+ suggested_kr_ticker: str = dspy.OutputField()
41
+
42
+ kr_extractor = dspy.Predict(ExtractKRStock)
43
+
44
+ def extract_ticker(user_input: str) -> str:
45
+ result = ticker_extractor(user_input=user_input)
46
+ ticker = result.suggested_ticker.strip().upper()
47
+
48
+ # ๋ฏธ๊ตญ์‹ ๋˜๋Š” ํ•œ๊ตญ์‹ ํ‹ฐ์ปค๋ฉด ๊ทธ๋Œ€๋กœ
49
+ if re.fullmatch(r"[A-Z]{1,5}(\.[A-Z]{2})?", ticker) or re.fullmatch(r"\d{6}\.KS", ticker):
50
+ return ticker
51
+
52
+ # ์•„๋‹ˆ๋ฉด ํ•œ๊ธ€ ๊ธฐ์—…๋ช… ์ถ”์ • โ†’ ํ•œ๊ตญ ํ‹ฐ์ปค ์ถ”์ถœ
53
+ kr_result = kr_extractor(user_input=user_input)
54
+ return kr_result.suggested_kr_ticker.strip().upper()
55
+
56
+ # === ๋‰ด์Šค ์ˆ˜์ง‘ ===
57
+ def get_yahoo_news(ticker: str):
58
+ try:
59
+ url = f"https://feeds.finance.yahoo.com/rss/2.0/headline?s={ticker}&region=US&lang=en-US"
60
+ soup = BeautifulSoup(requests.get(url).content, "xml")
61
+ one_week_ago = datetime.now(timezone.utc) - timedelta(days=7)
62
+ return [
63
+ {
64
+ "title": i.title.text,
65
+ "source": "Yahoo Finance",
66
+ "link": i.link.text
67
+ }
68
+ for i in soup.find_all("item")
69
+ if datetime.strptime(i.pubDate.text, "%a, %d %b %Y %H:%M:%S %z") >= one_week_ago
70
+ ][:6]
71
+ except:
72
+ return []
73
+
74
+ # === ์ฃผ๊ฐ€ ์ •๋ณด ์ˆ˜์ง‘ ===
75
+ def fetch_stock_info(ticker: str):
76
+ try:
77
+ stock = yf.Ticker(ticker)
78
+ hist = stock.history(period="1d")
79
+ info = stock.info
80
+ if hist.empty or "longName" not in info:
81
+ return None
82
+ return {
83
+ "company": info["longName"],
84
+ "price": round(hist["Close"].iloc[-1], 2),
85
+ "change_percent": round((hist["Close"].iloc[-1] - info.get("previousClose", 0)) / max(info.get("previousClose", 1), 1) * 100, 2)
86
+ }
87
+ except:
88
+ return None
89
+
90
+ # === ๋ฆฌ์Šคํฌ ์Šค์ฝ”์–ด๋ง ์„œ๋ช… ===
91
+ class StructuredRiskScoringSignature(dspy.Signature):
92
+ stock_info: str = dspy.InputField()
93
+ news: str = dspy.InputField()
94
+ overvaluation_score: str = dspy.OutputField()
95
+ overvaluation_reasoning: str = dspy.OutputField()
96
+ poor_earnings_score: str = dspy.OutputField()
97
+ poor_earnings_reasoning: str = dspy.OutputField()
98
+ financial_instability_score: str = dspy.OutputField()
99
+ financial_instability_reasoning: str = dspy.OutputField()
100
+ theme_overheating_score: str = dspy.OutputField()
101
+ theme_overheating_reasoning: str = dspy.OutputField()
102
+ recurring_negatives_score: str = dspy.OutputField()
103
+ recurring_negatives_reasoning: str = dspy.OutputField()
104
+ selloff_score: str = dspy.OutputField()
105
+ selloff_reasoning: str = dspy.OutputField()
106
+ total_score: str = dspy.OutputField()
107
+ risk_level: str = dspy.OutputField()
108
+ investment_message: str = dspy.OutputField()
109
+
110
+ risk_model = dspy.ChainOfThought(StructuredRiskScoringSignature)
111
+
112
+ def create_price_plot(ticker: str):
113
+ try:
114
+ stock = yf.Ticker(ticker)
115
+ hist = stock.history(period="1mo")
116
+
117
+ if hist.empty:
118
+ return None
119
+
120
+ fig = go.Figure()
121
+ fig.add_trace(go.Scatter(
122
+ x=hist.index,
123
+ y=hist["Close"],
124
+ mode='lines+markers',
125
+ name='Close Price',
126
+ line=dict(color='royalblue'),
127
+ marker=dict(size=6)
128
+ ))
129
+
130
+ fig.update_layout(
131
+ title=f"{ticker} Price Trend (1mo)",
132
+ xaxis_title="Date",
133
+ yaxis_title="Close Price ($)",
134
+ font=dict(
135
+ family="Arial, sans-serif",
136
+ size=14,
137
+ color="#333333"
138
+ ),
139
+ template="plotly_white",
140
+ height=300,
141
+ margin=dict(l=20, r=20, t=40, b=20),
142
+ plot_bgcolor="#fefbd8", # ๋‚ด๋ถ€
143
+ paper_bgcolor="#e0f7fa" # ์™ธ๋ถ€
144
+ )
145
+
146
+ return fig # ์ง์ ‘ ๋ฐ˜ํ™˜ (Gradio๊ฐ€ Plotly ์ง€์›ํ•จ)
147
+
148
+ except Exception as e:
149
+ print("Plotly price chart error:", e)
150
+ return None
151
+
152
+ # === ์‹œ๊ฐํ™” ===
153
+ from PIL import Image
154
+
155
+ def create_risk_plot(result, company_name, ticker, total_score, risk_level, investment_message):
156
+ categories = [
157
+ "Overval.", "Earnings", "Fin. Instab.",
158
+ "Theme OH", "Neg. News", "FII Sell"
159
+ ]
160
+ scores = list(map(int, [
161
+ result.overvaluation_score,
162
+ result.poor_earnings_score,
163
+ result.financial_instability_score,
164
+ result.theme_overheating_score,
165
+ result.recurring_negatives_score,
166
+ result.selloff_score
167
+ ]))
168
+
169
+ # ๐Ÿ“ ์ „์ฒด ๋ฐฐ๊ฒฝ์ƒ‰ (figure)
170
+ plt.figure(figsize=(10, 8), facecolor='#e0f7fa')
171
+
172
+ # ๐ŸŽจ AXES ๋ฐฐ๊ฒฝ์ƒ‰
173
+ ax = plt.gca()
174
+ ax.set_facecolor('#fefbd8') # ์˜ˆ: ํฐ์ƒ‰ ๋ฐฐ๊ฒฝ, ํ˜น์€ '#fefbd8', 'aliceblue' ๋“ฑ
175
+
176
+ bars = plt.bar(categories, scores, color='pink')
177
+ plt.ylim(0, 10)
178
+ plt.title(f"{company_name} ({ticker})", fontsize=30)
179
+ plt.ylabel("Score")
180
+ plt.xlabel("Category")
181
+
182
+ for bar in bars:
183
+ height = bar.get_height()
184
+ plt.text(bar.get_x() + bar.get_width() / 2, height + 0.2, str(height), ha='center', fontsize=12)
185
+
186
+ plt.text(
187
+ 0.5, 0.91,
188
+ f"Total Score: {total_score} / Risk Level: {risk_level}",
189
+ fontsize=20, ha='center', transform=plt.gca().transAxes
190
+ )
191
+
192
+ # ์—ฌ๋ฐฑ ์กฐ์ •
193
+ plt.subplots_adjust(top=0.85, bottom=0.10)
194
+
195
+ buf = io.BytesIO()
196
+ plt.savefig(buf, format='png')
197
+ buf.seek(0)
198
+ plt.close()
199
+
200
+ return Image.open(buf)
201
+
202
+ # === Gradio ํ•จ์ˆ˜ ===
203
+ def gradio_run(user_input: str):
204
+ ticker = extract_ticker(user_input)
205
+ if not ticker:
206
+ return "โŒ Unable to recognize a valid stock ticker from your input.", None
207
+
208
+ stock = fetch_stock_info(ticker)
209
+ if not stock:
210
+ return f"โŒ Failed to retrieve stock information for: {ticker}", None
211
+
212
+ news = get_yahoo_news(ticker)
213
+
214
+ if news:
215
+ news_html = "<h4>๐Ÿ“ฐ Cho's Pick Headlines (Recent 7 Days)</h4><ul>" + "".join(
216
+ f"<li><a href='{n['link']}' target='_blank'>{n['title']}</a> ({n['source']})</li>"
217
+ for n in news
218
+ ) + "</ul>"
219
+ else:
220
+ news_html = "<h4>๐Ÿ“ฐ Cho's Pick Headlines</h4><p>(No recent news available)</p>"
221
+
222
+ stock_text = f"Company: {stock['company']}\nPrice: ${stock['price']} ({stock['change_percent']}%)"
223
+
224
+ result = risk_model(stock_info=stock_text, news=news_html)
225
+
226
+ summary = f"""๐Ÿ“Œ Ticker: {stock['company']} ({ticker})
227
+ ๐Ÿงฎ Total Score: {result.total_score}
228
+ โš ๏ธ Risk Level: {result.risk_level}
229
+ ๐Ÿ’ฌ Recommendation: {result.investment_message}"""
230
+
231
+ plot_img = create_risk_plot(
232
+ result, stock["company"], ticker, result.total_score,
233
+ result.risk_level, result.investment_message
234
+ )
235
+ price_img = create_price_plot(ticker)
236
+ # ๐Ÿง  ๋ฆฌ์Šคํฌ ํ•ญ๋ชฉ๋ณ„ ์„ค๋ช…
237
+ annotations = f"""๐Ÿง  Category-wise Reasoning:
238
+ 1๏ธโƒฃ Overvaluation: {result.overvaluation_reasoning}
239
+ 2๏ธโƒฃ Poor Earnings: {result.poor_earnings_reasoning}
240
+ 3๏ธโƒฃ Financial Instability: {result.financial_instability_reasoning}
241
+ 4๏ธโƒฃ Theme Overheating: {result.theme_overheating_reasoning}
242
+ 5๏ธโƒฃ Recurring Negatives: {result.recurring_negatives_reasoning}
243
+ 6๏ธโƒฃ FII Sell-off: {result.selloff_reasoning}
244
+ """
245
+
246
+ return price_img, news_html, summary, plot_img, annotations
247
+
248
+ # === Gradio ์ธํ„ฐํŽ˜์ด์Šค ์‹คํ–‰ ===
249
+ with gr.Blocks() as iface:
250
+ # โœ… ์—ฌ๊ธฐ์— ๋กœ๊ณ +์ œ๋ชฉ HTML ์‚ฝ์ž…
251
+ gr.HTML("""
252
+ <div style="display: flex; align-items: center; margin-bottom: 10px;">
253
+ <img src="https://www.hanyang.ac.kr/documents/20182/73809/HYU_logo_singlecolor_png.png/b8aabfbe-a488-437d-b4a5-bd616d1577da?t=1474070795276" style="height: 50px; margin-right: 10px;">
254
+ <h2 style="margin: 0;">HYU-Cho's 'Risk Scoring Model' for Retail Portfolios based on Chain-of-Thought</h2>
255
+ </div>
256
+ <p>Analyze and visualize the risk level of any stock using natural language input.</p>
257
+ """)
258
+
259
+ # โœ… ์•ˆ๋‚ด ๋ฌธ๊ตฌ ์ถ”๊ฐ€
260
+ with gr.Row():
261
+ gr.Markdown("๐Ÿ“Œ **Welcome to Cho's Risk Scoring Model. Enter a stock-related question to begin.**")
262
+
263
+ with gr.Row():
264
+ user_input = gr.Textbox(label="User Input", lines=2, placeholder="e.g. Is Tesla risky these days?")
265
+ submit_btn = gr.Button("Submit", variant="primary")
266
+ clear_btn = gr.Button("Clear")
267
+
268
+ # ๐Ÿ“ˆ ์ฃผ๊ฐ€ ํ๋ฆ„ ์ œ์ผ ์œ„๋กœ
269
+ output_price_plot = gr.Plot(label="๐Ÿ“ˆ 1-Month Price Trend")
270
+
271
+ # ๐Ÿ“ฐ ๋‰ด์Šค (HTML ํด๋ฆญ ๊ฐ€๋Šฅ + ํƒ€์ดํ‹€)
272
+ output_news_only = gr.HTML(label=None) # ํ—ค๋“œ๋ผ์ธ ํƒ€์ดํ‹€์€ HTML ๋‚ด์—์„œ ์ง์ ‘ ํ‘œํ˜„
273
+
274
+ # ๐Ÿ“‹ ์š”์•ฝ
275
+ output_summary = gr.Textbox(label="๐Ÿ“‹ Portfolio Risk Evaluation by CoT", lines=6)
276
+
277
+ # ๐Ÿ“Š ์œ„ํ—˜๋„ ์‹œ๊ฐํ™” + ์ƒ์„ธ ์„ค๋ช…
278
+ with gr.Row():
279
+ output_plot = gr.Image(label="๐Ÿ“Š Risk Score Visualization")
280
+ output_detail = gr.Textbox(label="๐Ÿง  Detailed Risk Reasoning", lines=25)
281
+
282
+ # ์‹คํ–‰
283
+ submit_btn.click(fn=gradio_run, inputs=user_input,
284
+ outputs=[output_price_plot, output_news_only, output_summary, output_plot, output_detail])
285
+
286
+ clear_btn.click(lambda: ("", "", "", None, ""), inputs=[],
287
+ outputs=[output_price_plot, output_news_only, output_summary, output_plot, output_detail])
288
+
289
+ iface.launch()
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio
2
+ dspy
3
+ yfinance
4
+ requests
5
+ beautifulsoup4
6
+ plotly
7
+ matplotlib
8
+ pillow
9
+ openai
10
+ tiktoken
11
+ httpx