mr601s commited on
Commit
4875162
·
verified ·
1 Parent(s): f776566

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +438 -111
app.py CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import requests
3
  import numpy as np
@@ -8,9 +16,15 @@ import random
8
  from datetime import datetime
9
  import os
10
 
 
 
 
11
  POLYGON_API_KEY = os.getenv("POLYGON_API_KEY") or "fAhg47wPlf4FT6U2Hn23kQoQCQIyW0G_"
12
- FINNHUB_API_KEY = "d2urs69r01qq994h1f5gd2urs69r01qq994h1f60"
13
 
 
 
 
14
  def fetch_polygon_quote(ticker, polygon_api_key=POLYGON_API_KEY):
15
  url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.upper()}/prev?adjusted=true&apiKey={polygon_api_key}"
16
  try:
@@ -18,10 +32,13 @@ def fetch_polygon_quote(ticker, polygon_api_key=POLYGON_API_KEY):
18
  response.raise_for_status()
19
  data = response.json()
20
  if data.get("results"):
21
- last = data["results"][0]
22
- price = last["c"]
23
- close_dt = datetime.utcfromtimestamp(last["t"] / 1000).strftime('%Y-%m-%d')
24
- return f"💰 **Previous Close for {ticker.upper()} (as of {close_dt})**\n\n• **Close Price:** ${price:.2f}\n\n_(Free Polygon plan only provides prior close)_"
 
 
 
25
  else:
26
  return f"❌ Quote data unavailable for {ticker.upper()}."
27
  except Exception as e:
@@ -32,68 +49,72 @@ def get_financial_summary_finnhub(ticker, finnhub_api_key=FINNHUB_API_KEY):
32
  try:
33
  response = requests.get(url, timeout=10)
34
  response.raise_for_status()
35
- data = response.json()
36
- metrics = data.get('metric', {})
37
  if not metrics:
38
  return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ No financial data found."
39
  result = f"📊 **Financial Summary for {ticker.upper()}**\n\n"
40
- if metrics.get('totalRevenueTTM'):
41
  result += f"• **Revenue (TTM):** ${int(metrics['totalRevenueTTM']):,}\n"
42
- if metrics.get('netIncomeTTM'):
43
  result += f"• **Net Income (TTM):** ${int(metrics['netIncomeTTM']):,}\n"
44
  pe = metrics.get('peNormalizedAnnual') or metrics.get('peExclExtraTTM')
45
  if pe is not None:
46
- result += f"• **P/E Ratio:** {pe:.2f}\n"
47
  pb = metrics.get('pbAnnual')
48
  if pb is not None:
49
- result += f"• **P/B Ratio:** {pb:.2f}\n"
50
  dy = metrics.get('dividendYieldIndicatedAnnual')
51
  if dy is not None:
52
- result += f"• **Dividend Yield:** {dy:.2f}%\n"
53
  dte = metrics.get('totalDebt/totalEquityAnnual')
54
  if dte is not None:
55
- result += f"• **Debt/Equity:** {dte:.2f}\n"
56
  pm = metrics.get('netProfitMarginTTM')
57
  if pm is not None:
58
- result += f"• **Net Profit Margin:** {pm:.2f}%\n"
59
  mc = metrics.get('marketCapitalization')
60
  if mc is not None:
61
  result += f"• **Market Cap:** ${int(mc):,}\n"
62
  if result.strip() == f"📊 **Financial Summary for {ticker.upper()}**":
63
- return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ No data available from Finnhub."
64
  return result
65
  except Exception as e:
66
  return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ Error fetching financial summary: {e}"
67
 
 
 
 
68
  class SECUtils:
69
  def __init__(self):
70
  self.cik_lookup_url = "https://www.sec.gov/files/company_tickers.json"
71
  self.edgar_search_url = "https://data.sec.gov/submissions/CIK{cik}.json"
72
- self.headers = {"User-Agent": "StockResearchMVP/1.0 (educational@example.com)"}
 
73
  def get_cik(self, ticker):
74
  try:
75
  time.sleep(0.5)
76
  response = requests.get(self.cik_lookup_url, headers=self.headers, timeout=20)
77
- if response.status_code != 200:
78
- return None
79
  data = response.json()
80
- for k, v in data.items():
81
  if isinstance(v, dict) and v.get('ticker', '').upper() == ticker.upper():
82
  return str(v['cik_str']).zfill(10)
83
  return None
84
  except Exception as e:
85
  print(f"CIK lookup error: {e}")
86
  return None
 
87
  def get_recent_filings(self, ticker):
88
  try:
89
  cik = self.get_cik(ticker)
90
  if not cik:
91
- return f"📄 **SEC Filings for {ticker}**\n\n❌ CIK not found. This may be a newer company or ticker not in SEC database.\n\n💡 Try checking [SEC EDGAR directly](https://www.sec.gov/edgar/search/) for this company."
92
  time.sleep(0.5)
93
  url = self.edgar_search_url.format(cik=cik)
94
  response = requests.get(url, headers=self.headers, timeout=20)
95
  if response.status_code != 200:
96
- return f"📄 **SEC Filings for {ticker}**\n\n❌ Unable to fetch SEC data (Status: {response.status_code}).\n\n💡 Try [SEC EDGAR search](https://www.sec.gov/edgar/search/) directly."
97
  data = response.json()
98
  filings = data.get('filings', {}).get('recent', {})
99
  if not filings or not filings.get('form'):
@@ -112,18 +133,22 @@ class SECUtils:
112
  result += f" 📎 [View Filing]({filing_url})\n\n"
113
  return result
114
  except Exception as e:
115
- return f"📄 **SEC Filings for {ticker}**\n\n❌ Error fetching SEC filings: {str(e)}\n\n💡 Try [SEC EDGAR search](https://www.sec.gov/edgar/search/) directly."
116
 
 
 
 
117
  class NewsUtils:
118
  def __init__(self):
119
- self.headers = {"User-Agent": "StockResearchMVP/1.0 (educational@example.com)"}
 
120
  def get_yahoo_news(self, ticker):
121
  try:
122
  time.sleep(random.uniform(0.5, 1.0))
123
  url = f"https://feeds.finance.yahoo.com/rss/2.0/headline?s={ticker}&region=US&lang=en-US"
124
  feed = feedparser.parse(url)
125
  if not feed.entries:
126
- return f"📰 **Latest News for {ticker}**\n\n❌ No recent news found via RSS feed.\n\n💡 Try these alternatives:\n• [Yahoo Finance News](https://finance.yahoo.com/quote/{ticker}/news)\n• [Google Finance](https://www.google.com/finance/quote/{ticker}:NASDAQ)\n• [MarketWatch](https://www.marketwatch.com/investing/stock/{ticker})"
127
  result = f"📰 **Latest News for {ticker}**\n\n"
128
  for i, entry in enumerate(feed.entries[:5]):
129
  title = getattr(entry, 'title', 'No title')
@@ -134,28 +159,33 @@ class NewsUtils:
134
  result += f" 🔗 [Read More]({link})\n\n"
135
  return result
136
  except Exception as e:
137
- return f"📰 **Latest News for {ticker}**\n\n❌ Error fetching news: {str(e)}\n\n💡 Try these alternatives:\n• [Yahoo Finance News](https://finance.yahoo.com/quote/{ticker}/news)\n• [Google Finance](https://www.google.com/finance/quote/{ticker}:NASDAQ)\n• [MarketWatch](https://www.marketwatch.com/investing/stock/{ticker})"
138
 
 
 
 
139
  def get_tradingview_embed(ticker):
140
- ticker = ticker.strip().upper() if ticker else "AAPL"
141
  ticker = ''.join(filter(str.isalnum, ticker))
142
  return f'<iframe src="https://s.tradingview.com/widgetembed/?symbol={ticker}&interval=D&hidesidetoolbar=1&theme=light" width="100%" height="400" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
143
 
 
 
 
144
  def simulate_order_book(side, order_type, price, size, seed=123):
145
- np.random.seed(seed)
146
  base_price = 100.00
147
  levels = np.arange(base_price - 2, base_price + 2.5, 0.5)
148
  buy_sizes = np.random.randint(1, 40, len(levels))
149
  sell_sizes = np.random.randint(1, 40, len(levels))
 
150
  buy_mask = levels < base_price
151
  sell_mask = levels > base_price
152
  buys = np.where(buy_mask, buy_sizes, 0)
153
  sells = np.where(sell_mask, sell_sizes, 0)
154
- df = pd.DataFrame({
155
- 'Price': levels,
156
- 'Buy Size': buys,
157
- 'Sell Size': sells
158
- }).sort_values(by='Price', ascending=False).reset_index(drop=True)
159
  fill_msg = ""
160
  if order_type == "Market":
161
  if side == "Buy":
@@ -170,8 +200,8 @@ def simulate_order_book(side, order_type, price, size, seed=123):
170
  if side == "Buy":
171
  if price >= df['Price'].min():
172
  sells_at_or_below = df[(df['Price'] <= price) & (df['Sell Size'] > 0)]
173
- if sells_at_or_below.shape[0]:
174
- fill_price = sells_at_or_below.iloc[0]['Price']
175
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Buy)"
176
  else:
177
  queue_spot = 1 + np.random.randint(0, 3)
@@ -181,8 +211,8 @@ def simulate_order_book(side, order_type, price, size, seed=123):
181
  else:
182
  if price <= df['Price'].max():
183
  buys_at_or_above = df[(df['Price'] >= price) & (df['Buy Size'] > 0)]
184
- if buys_at_or_above.shape[0]:
185
- fill_price = buys_at_or_above.iloc[0]['Price']
186
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Sell)"
187
  else:
188
  queue_spot = 1 + np.random.randint(0, 3)
@@ -192,34 +222,90 @@ def simulate_order_book(side, order_type, price, size, seed=123):
192
  return df, fill_msg
193
 
194
  def slippage_estimator(side, order_size, seed=123):
195
- np.random.seed(seed)
196
  base_price = 100
197
- levels = np.arange(base_price-2, base_price+2.5, 0.5)
 
198
  if side == "Buy":
199
- sizes = np.random.randint(10, 70, len(levels))
200
- prices = levels[levels > base_price]
201
- sizes = sizes[levels > base_price]
 
202
  else:
203
- sizes = np.random.randint(10, 70, len(levels))
204
- prices = levels[levels < base_price]
205
- sizes = sizes[levels < base_price]
206
- remaining = order_size
 
 
 
 
 
207
  fills = []
208
  for p, s in zip(prices, sizes):
209
- take = min(s, remaining)
210
- fills.append((p, take))
211
- remaining -= take
 
212
  if remaining <= 0:
213
  break
 
214
  if remaining > 0:
215
  return "Not enough liquidity to fill order!", None
 
216
  df = pd.DataFrame(fills, columns=["Price", "Shares"])
217
- avg_fill = (df["Price"] * df["Shares"]).sum() / order_size
218
  slip = avg_fill - base_price if side == "Buy" else base_price - avg_fill
219
  slip_pct = (slip / base_price) * 100
220
  summary = f"Est. avg fill @ {avg_fill:.2f}; Slippage: {slip:.2f} ({slip_pct:.2f}%) from ideal {base_price}"
221
  return summary, df
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  sec_utils = SECUtils()
224
  news_utils = NewsUtils()
225
 
@@ -239,23 +325,24 @@ def update_stock_info(ticker):
239
  chart_html = get_tradingview_embed(ticker)
240
  return quote_data, news_data, filings_data, financial_data, chart_html
241
 
 
 
 
242
  css = """
243
  .gradio-container {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1400px; margin: 0 auto;}
244
  .tab-nav button {font-size: 16px; font-weight: 600;}
245
  """
246
 
247
- with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Bullish Minds AI - Stock Research & Education Platform") as demo:
248
- gr.Image("logo.png", elem_id="header-logo", show_label=False, show_download_button=False)
 
249
  gr.Markdown("""
250
  # **Bullish Minds AI**
251
- *Stock Research Platform MVP*
252
-
253
- **Comprehensive stock analysis, real-time data, and interactive education modules.**
254
-
255
- 🎯 Enter a stock ticker symbol (**AAPL**, **TSLA**, **MSFT**, **GOOGL**) for market data, or check out the Lessons tab for learning modules!
256
 
257
- ⚠️ **Note**: Stock quote data from Polygon (Previous Close for free plans). Financial summary from Finnhub. Charts powered by TradingView.
258
  """)
 
259
  with gr.Row():
260
  with gr.Column(scale=3):
261
  ticker_input = gr.Textbox(
@@ -264,8 +351,10 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Bullish Minds AI - Stock
264
  value="AAPL"
265
  )
266
  with gr.Column(scale=1):
267
- refresh_btn = gr.Button("🔄 Refresh Data", variant="primary", size="lg")
 
268
  with gr.Tabs():
 
269
  with gr.TabItem("💰 Quote & Overview"):
270
  quote_output = gr.Markdown(value="Enter a ticker to see stock quote")
271
  with gr.TabItem("📰 News"):
@@ -278,64 +367,302 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Bullish Minds AI - Stock
278
  gr.Markdown("### Interactive Price Chart")
279
  gr.Markdown("*Powered by TradingView*")
280
  chart_output = gr.HTML(get_tradingview_embed("AAPL"))
281
- with gr.TabItem("🎓 Lessons"):
 
 
282
  with gr.Tabs():
283
- # ================= LESSON 1 =================
284
- with gr.TabItem("Lesson 1: Market Venues Explained"):
285
- gr.Markdown("## Lesson 1 — Market Venues: Exchanges, Dark Pools, Auction vs. Dealer Markets")
286
- # FULL Accordions and Markdown for Lesson 1 here (see earlier completions)
287
- # ... [lesson content omitted for brevity - paste Lesson 1 text from previous completions here]
288
- gr.Markdown("---")
289
- gr.Markdown("*Try the tools below to visualize order book mechanics and slippage:*")
 
 
 
 
 
 
290
  with gr.Tabs():
291
- with gr.TabItem("Order Book Simulator"):
292
- lesson1_order = gr.Interface(
293
- fn=simulate_order_book,
294
- inputs=[
295
- gr.Dropdown(["Buy", "Sell"], label="Order Side"),
296
- gr.Dropdown(["Market", "Limit"], label="Order Type"),
297
- gr.Number(value=100.00, label="Order Price (for limit)"),
298
- gr.Slider(1, 100, value=10, step=1, label="Order Size"),
299
- gr.Number(value=123, label="Seed (optional, for replay)")
300
- ],
301
- outputs=[
302
- gr.Dataframe(label="Order Book (randomized)"),
303
- gr.Textbox(label="Result / Fill Message")
304
- ],
305
- live=False,
306
- allow_flagging="never"
307
- )
308
- with gr.TabItem("Slippage Estimator"):
309
- lesson1_slippage = gr.Interface(
310
- fn=slippage_estimator,
311
- inputs=[
312
- gr.Dropdown(["Buy", "Sell"], label="Order Side"),
313
- gr.Slider(1, 300, value=50, step=1, label="Order Size"),
314
- gr.Number(value=123, label="Seed (for repeatability)")
315
- ],
316
- outputs=[
317
- gr.Textbox(label="Estimate"),
318
- gr.Dataframe(label="Fill breakdown")
319
- ],
320
- live=False,
321
- allow_flagging="never"
 
 
 
 
 
 
 
 
 
322
  )
323
- # ================= LESSON 2 =================
324
- with gr.TabItem("Lesson 2: Tickers, Floats, Market Cap, Sectors, Indices"):
325
- gr.Markdown("## Lesson 2 Tickers, Floats, Market Cap, Sectors, Indices")
326
- # FULL Accordions and Markdown for Lesson 2 here (see earlier completions)
327
- # ... [lesson content omitted for brevity - paste Lesson 2 text from previous completions here]
328
- # ================= LESSON 3 =================
329
- with gr.TabItem("Lesson 3: Stocks, ETFs, ADRs, and Derivatives"):
330
- gr.Markdown("## Lesson 3 — Stocks vs. ETFs vs. ADRs; Basics of Options and Futures")
331
- # FULL Accordions and Markdown for Lesson 3 here (see earlier completions)
332
- # ... [lesson content omitted for brevity - paste Lesson 3 text from previous completions here]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
  gr.Markdown("""
335
  ---
336
- **Data Sources:** Polygon.io for quotes, Finnhub for financials, Yahoo RSS for news, SEC EDGAR for filings.
337
-
338
- **Troubleshooting:** If you encounter errors, double-check your ticker or wait and retry.
339
  """)
340
 
341
  ticker_input.change(
@@ -346,7 +673,7 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Bullish Minds AI - Stock
346
  refresh_btn.click(
347
  fn=update_stock_info,
348
  inputs=[ticker_input],
349
- outputs=[quote_output, news_output, filings_output, financial_output, chart_output]
350
  )
351
 
352
  if __name__ == "__main__":
 
1
+ # app.py — Bullish Minds AI: Stock Research + Trading Curriculum
2
+ # Run: pip install gradio requests numpy pandas feedparser
3
+ # Launch: python app.py
4
+ # Notes:
5
+ # - Polygon on free plan: previous close endpoint used.
6
+ # - Finnhub metrics endpoint requires token.
7
+ # - SEC requires descriptive User-Agent.
8
+
9
  import gradio as gr
10
  import requests
11
  import numpy as np
 
16
  from datetime import datetime
17
  import os
18
 
19
+ # =========================
20
+ # API Keys (ENV preferred)
21
+ # =========================
22
  POLYGON_API_KEY = os.getenv("POLYGON_API_KEY") or "fAhg47wPlf4FT6U2Hn23kQoQCQIyW0G_"
23
+ FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY") or "d2urs69r01qq994h1f5gd2urs69r01qq994h1f60"
24
 
25
+ # =========================
26
+ # Data Fetchers
27
+ # =========================
28
  def fetch_polygon_quote(ticker, polygon_api_key=POLYGON_API_KEY):
29
  url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.upper()}/prev?adjusted=true&apiKey={polygon_api_key}"
30
  try:
 
32
  response.raise_for_status()
33
  data = response.json()
34
  if data.get("results"):
35
+ last = data["results"]
36
+ price = last.get("c")
37
+ ts = last.get("t")
38
+ if price is None or ts is None:
39
+ return f"❌ Quote format unexpected for {ticker.upper()}."
40
+ close_dt = datetime.utcfromtimestamp(ts / 1000).strftime('%Y-%m-%d')
41
+ return f"💰 **Previous Close for {ticker.upper()} (as of {close_dt})**\n\n• **Close Price:** ${price:.2f}\n\n_(Free Polygon plan provides prior close)_"
42
  else:
43
  return f"❌ Quote data unavailable for {ticker.upper()}."
44
  except Exception as e:
 
49
  try:
50
  response = requests.get(url, timeout=10)
51
  response.raise_for_status()
52
+ data = response.json() or {}
53
+ metrics = data.get('metric', {}) or {}
54
  if not metrics:
55
  return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ No financial data found."
56
  result = f"📊 **Financial Summary for {ticker.upper()}**\n\n"
57
+ if metrics.get('totalRevenueTTM') is not None:
58
  result += f"• **Revenue (TTM):** ${int(metrics['totalRevenueTTM']):,}\n"
59
+ if metrics.get('netIncomeTTM') is not None:
60
  result += f"• **Net Income (TTM):** ${int(metrics['netIncomeTTM']):,}\n"
61
  pe = metrics.get('peNormalizedAnnual') or metrics.get('peExclExtraTTM')
62
  if pe is not None:
63
+ result += f"• **P/E Ratio:** {float(pe):.2f}\n"
64
  pb = metrics.get('pbAnnual')
65
  if pb is not None:
66
+ result += f"• **P/B Ratio:** {float(pb):.2f}\n"
67
  dy = metrics.get('dividendYieldIndicatedAnnual')
68
  if dy is not None:
69
+ result += f"• **Dividend Yield:** {float(dy):.2f}%\n"
70
  dte = metrics.get('totalDebt/totalEquityAnnual')
71
  if dte is not None:
72
+ result += f"• **Debt/Equity:** {float(dte):.2f}\n"
73
  pm = metrics.get('netProfitMarginTTM')
74
  if pm is not None:
75
+ result += f"• **Net Profit Margin:** {float(pm):.2f}%\n"
76
  mc = metrics.get('marketCapitalization')
77
  if mc is not None:
78
  result += f"• **Market Cap:** ${int(mc):,}\n"
79
  if result.strip() == f"📊 **Financial Summary for {ticker.upper()}**":
80
+ return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ No data available."
81
  return result
82
  except Exception as e:
83
  return f"📊 **Financial Summary for {ticker.upper()}**\n\n❌ Error fetching financial summary: {e}"
84
 
85
+ # =========================
86
+ # SEC Utilities
87
+ # =========================
88
  class SECUtils:
89
  def __init__(self):
90
  self.cik_lookup_url = "https://www.sec.gov/files/company_tickers.json"
91
  self.edgar_search_url = "https://data.sec.gov/submissions/CIK{cik}.json"
92
+ self.headers = {"User-Agent": "BullishMindsAI/1.0 (marcus@bullishmindsai.co.site)"}
93
+
94
  def get_cik(self, ticker):
95
  try:
96
  time.sleep(0.5)
97
  response = requests.get(self.cik_lookup_url, headers=self.headers, timeout=20)
98
+ response.raise_for_status()
 
99
  data = response.json()
100
+ for _, v in data.items():
101
  if isinstance(v, dict) and v.get('ticker', '').upper() == ticker.upper():
102
  return str(v['cik_str']).zfill(10)
103
  return None
104
  except Exception as e:
105
  print(f"CIK lookup error: {e}")
106
  return None
107
+
108
  def get_recent_filings(self, ticker):
109
  try:
110
  cik = self.get_cik(ticker)
111
  if not cik:
112
+ return f"📄 **SEC Filings for {ticker}**\n\n❌ CIK not found. May be a newer ticker.\n\n💡 Try SEC EDGAR search."
113
  time.sleep(0.5)
114
  url = self.edgar_search_url.format(cik=cik)
115
  response = requests.get(url, headers=self.headers, timeout=20)
116
  if response.status_code != 200:
117
+ return f"📄 **SEC Filings for {ticker}**\n\n❌ Unable to fetch SEC data (Status: {response.status_code})."
118
  data = response.json()
119
  filings = data.get('filings', {}).get('recent', {})
120
  if not filings or not filings.get('form'):
 
133
  result += f" 📎 [View Filing]({filing_url})\n\n"
134
  return result
135
  except Exception as e:
136
+ return f"📄 **SEC Filings for {ticker}**\n\n❌ Error fetching SEC filings: {str(e)}"
137
 
138
+ # =========================
139
+ # News Utilities (Yahoo RSS)
140
+ # =========================
141
  class NewsUtils:
142
  def __init__(self):
143
+ self.headers = {"User-Agent": "BullishMindsAI/1.0 (marcus@bullishmindsai.co.site)"}
144
+
145
  def get_yahoo_news(self, ticker):
146
  try:
147
  time.sleep(random.uniform(0.5, 1.0))
148
  url = f"https://feeds.finance.yahoo.com/rss/2.0/headline?s={ticker}&region=US&lang=en-US"
149
  feed = feedparser.parse(url)
150
  if not feed.entries:
151
+ return f"📰 **Latest News for {ticker}**\n\n❌ No recent news found via RSS feed."
152
  result = f"📰 **Latest News for {ticker}**\n\n"
153
  for i, entry in enumerate(feed.entries[:5]):
154
  title = getattr(entry, 'title', 'No title')
 
159
  result += f" 🔗 [Read More]({link})\n\n"
160
  return result
161
  except Exception as e:
162
+ return f"📰 **Latest News for {ticker}**\n\n❌ Error fetching news: {str(e)}"
163
 
164
+ # =========================
165
+ # Chart Embed (TradingView)
166
+ # =========================
167
  def get_tradingview_embed(ticker):
168
+ ticker = (ticker or "AAPL").strip().upper()
169
  ticker = ''.join(filter(str.isalnum, ticker))
170
  return f'<iframe src="https://s.tradingview.com/widgetembed/?symbol={ticker}&interval=D&hidesidetoolbar=1&theme=light" width="100%" height="400" frameborder="0" allowtransparency="true" scrolling="no"></iframe>'
171
 
172
+ # =========================
173
+ # Simulators/Calculators (Existing)
174
+ # =========================
175
  def simulate_order_book(side, order_type, price, size, seed=123):
176
+ np.random.seed(int(seed) if seed is not None else 123)
177
  base_price = 100.00
178
  levels = np.arange(base_price - 2, base_price + 2.5, 0.5)
179
  buy_sizes = np.random.randint(1, 40, len(levels))
180
  sell_sizes = np.random.randint(1, 40, len(levels))
181
+
182
  buy_mask = levels < base_price
183
  sell_mask = levels > base_price
184
  buys = np.where(buy_mask, buy_sizes, 0)
185
  sells = np.where(sell_mask, sell_sizes, 0)
186
+
187
+ df = pd.DataFrame({'Price': levels, 'Buy Size': buys, 'Sell Size': sells}).sort_values(by='Price', ascending=False).reset_index(drop=True)
188
+
 
 
189
  fill_msg = ""
190
  if order_type == "Market":
191
  if side == "Buy":
 
200
  if side == "Buy":
201
  if price >= df['Price'].min():
202
  sells_at_or_below = df[(df['Price'] <= price) & (df['Sell Size'] > 0)]
203
+ if not sells_at_or_below.empty:
204
+ fill_price = sells_at_or_below.iloc['Price']
205
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Buy)"
206
  else:
207
  queue_spot = 1 + np.random.randint(0, 3)
 
211
  else:
212
  if price <= df['Price'].max():
213
  buys_at_or_above = df[(df['Price'] >= price) & (df['Buy Size'] > 0)]
214
+ if not buys_at_or_above.empty:
215
+ fill_price = buys_at_or_above.iloc['Price']
216
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Sell)"
217
  else:
218
  queue_spot = 1 + np.random.randint(0, 3)
 
222
  return df, fill_msg
223
 
224
  def slippage_estimator(side, order_size, seed=123):
225
+ np.random.seed(int(seed) if seed is not None else 123)
226
  base_price = 100
227
+ levels = np.arange(base_price - 2, base_price + 2.5, 0.5)
228
+
229
  if side == "Buy":
230
+ sizes_all = np.random.randint(10, 70, len(levels))
231
+ mask = levels > base_price
232
+ prices = levels[mask]
233
+ sizes = sizes_all[mask]
234
  else:
235
+ sizes_all = np.random.randint(10, 70, len(levels))
236
+ mask = levels < base_price
237
+ prices = levels[mask]
238
+ sizes = sizes_all[mask]
239
+
240
+ remaining = int(order_size)
241
+ if remaining <= 0:
242
+ return "Order size must be > 0", None
243
+
244
  fills = []
245
  for p, s in zip(prices, sizes):
246
+ take = min(int(s), remaining)
247
+ if take > 0:
248
+ fills.append((p, take))
249
+ remaining -= take
250
  if remaining <= 0:
251
  break
252
+
253
  if remaining > 0:
254
  return "Not enough liquidity to fill order!", None
255
+
256
  df = pd.DataFrame(fills, columns=["Price", "Shares"])
257
+ avg_fill = (df["Price"] * df["Shares"]).sum() / df["Shares"].sum()
258
  slip = avg_fill - base_price if side == "Buy" else base_price - avg_fill
259
  slip_pct = (slip / base_price) * 100
260
  summary = f"Est. avg fill @ {avg_fill:.2f}; Slippage: {slip:.2f} ({slip_pct:.2f}%) from ideal {base_price}"
261
  return summary, df
262
 
263
+ # =========================
264
+ # New Curriculum Calculators
265
+ # =========================
266
+ def rr_position_size_calc(account_equity: float, risk_pct: float, entry: float, stop: float) -> str:
267
+ if account_equity <= 0 or risk_pct <= 0:
268
+ return "Inputs must be positive."
269
+ risk_dollars = account_equity * (risk_pct / 100.0)
270
+ per_share_risk = max(1e-6, abs(entry - stop))
271
+ shares = int(risk_dollars // per_share_risk)
272
+ rr2_target = entry + 2 * (entry - stop) if entry > stop else entry - 2 * (stop - entry)
273
+ rr3_target = entry + 3 * (entry - stop) if entry > stop else entry - 3 * (stop - entry)
274
+ return (
275
+ f"Risk: ${risk_dollars:,.2f}\n"
276
+ f"Max shares: {shares:,}\n"
277
+ f"Targets: 2R={rr2_target:.2f}, 3R={rr3_target:.2f}"
278
+ )
279
+
280
+ def atr_stop_calc(entry: float, atr: float, atr_mult: float, direction: str) -> str:
281
+ if atr <= 0 or atr_mult <= 0:
282
+ return "ATR and multiplier must be > 0."
283
+ if direction == "Long":
284
+ stop = entry - atr_mult * atr
285
+ else:
286
+ stop = entry + atr_mult * atr
287
+ return f"Suggested stop: {stop:.2f}"
288
+
289
+ def expectancy_calc(win_rate_pct: float, avg_win: float, avg_loss: float) -> str:
290
+ p = max(0.0, min(1.0, win_rate_pct / 100.0))
291
+ if avg_win < 0 or avg_loss <= 0:
292
+ return "Avg win must be >= 0 and avg loss > 0."
293
+ exp = p * avg_win - (1 - p) * avg_loss
294
+ return f"Expectancy per trade: {exp:.2f}"
295
+
296
+ def risk_of_ruin_estimator(win_rate_pct: float, reward_risk: float, bankroll_risk_pct: float) -> str:
297
+ p = max(0.0, min(1.0, win_rate_pct / 100.0))
298
+ r = max(1e-6, reward_risk)
299
+ b = max(1e-6, bankroll_risk_pct / 100.0)
300
+ edge = p * r - (1 - p)
301
+ if edge <= 0:
302
+ return "High risk of ruin (negative/zero edge)."
303
+ approx_ror = max(0.0, min(1.0, (1 - edge) ** (1 / b)))
304
+ return f"Heuristic risk of ruin: {approx_ror*100:.1f}% (educational)"
305
+
306
+ # =========================
307
+ # Orchestration
308
+ # =========================
309
  sec_utils = SECUtils()
310
  news_utils = NewsUtils()
311
 
 
325
  chart_html = get_tradingview_embed(ticker)
326
  return quote_data, news_data, filings_data, financial_data, chart_html
327
 
328
+ # =========================
329
+ # UI
330
+ # =========================
331
  css = """
332
  .gradio-container {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1400px; margin: 0 auto;}
333
  .tab-nav button {font-size: 16px; font-weight: 600;}
334
  """
335
 
336
+ with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Bullish Minds AI - Stock Research & Education") as demo:
337
+ if os.path.exists("logo.png"):
338
+ gr.Image("logo.png", elem_id="header-logo", show_label=False, show_download_button=False)
339
  gr.Markdown("""
340
  # **Bullish Minds AI**
341
+ Stock Research Platform + Trading Curriculum
 
 
 
 
342
 
343
+ Honest, educational, and data‑rich. Use quotes, news, filings, and charts alongside interactive lessons. Not financial advice.
344
  """)
345
+
346
  with gr.Row():
347
  with gr.Column(scale=3):
348
  ticker_input = gr.Textbox(
 
351
  value="AAPL"
352
  )
353
  with gr.Column(scale=1):
354
+ refresh_btn = gr.Button("🔄 Refresh Data", variant="primary")
355
+
356
  with gr.Tabs():
357
+ # ========== Research Tabs ==========
358
  with gr.TabItem("💰 Quote & Overview"):
359
  quote_output = gr.Markdown(value="Enter a ticker to see stock quote")
360
  with gr.TabItem("📰 News"):
 
367
  gr.Markdown("### Interactive Price Chart")
368
  gr.Markdown("*Powered by TradingView*")
369
  chart_output = gr.HTML(get_tradingview_embed("AAPL"))
370
+
371
+ # ========== Education Tabs ==========
372
+ with gr.TabItem("🎓 Education"):
373
  with gr.Tabs():
374
+ # Orientation
375
+ with gr.TabItem("0) Orientation"):
376
+ gr.Markdown("""
377
+ ## Orientation & Disclaimers
378
+ - Educational purpose only; not financial advice. Trading involves risk of loss of principal.
379
+ - Suggested paths: Beginner → Foundations + Risk/Psych → Day or Swing → Validation → Compliance; Long‑Term path for investors.
380
+ - Time guide: Foundations (3–5 hrs), Risk/Psych (2–3 hrs), Track (6–12 hrs), Validation/Compliance (3–5 hrs), Capstones (varies).
381
+ """)
382
+ gr.Markdown("### How to use")
383
+ gr.Markdown("- Read lessons, use calculators/simulators, complete quizzes and assignments. Journal and review weekly.")
384
+
385
+ # Foundations
386
+ with gr.TabItem("1) Foundations"):
387
  with gr.Tabs():
388
+ with gr.TabItem("1.1 Structure & Products"):
389
+ gr.Markdown("""
390
+ ### Market Structure & Products
391
+ - Exchanges vs dark pools; auction vs dealer markets; where liquidity lives.
392
+ - Tickers, float, market cap, sectors, indices; why breadth/rotation matter.
393
+ - Products: stocks, ETFs, ADRs; options/futures context only at this stage.
394
+ """)
395
+ gr.Markdown("#### Interactive: Order Book + Slippage")
396
+ gr.Markdown("- Use the Order Book Simulator and Slippage Estimator below to see fill behavior.")
397
+ with gr.Row():
398
+ with gr.Column():
399
+ gr.Interface(
400
+ fn=simulate_order_book,
401
+ inputs=[
402
+ gr.Dropdown(["Buy", "Sell"], label="Order Side"),
403
+ gr.Dropdown(["Market", "Limit"], label="Order Type"),
404
+ gr.Number(value=100.00, label="Order Price (for limit)"),
405
+ gr.Slider(1, 100, value=10, step=1, label="Order Size"),
406
+ gr.Number(value=123, label="Seed")
407
+ ],
408
+ outputs=[gr.Dataframe(), gr.Textbox(label="Fill Message")],
409
+ live=False,
410
+ allow_flagging="never"
411
+ )
412
+ with gr.Column():
413
+ gr.Interface(
414
+ fn=slippage_estimator,
415
+ inputs=[
416
+ gr.Dropdown(["Buy", "Sell"], label="Order Side"),
417
+ gr.Slider(1, 300, value=50, step=1, label="Order Size"),
418
+ gr.Number(value=123, label="Seed")
419
+ ],
420
+ outputs=[gr.Textbox(label="Estimate"), gr.Dataframe()],
421
+ live=False,
422
+ allow_flagging="never"
423
+ )
424
+ gr.Markdown("#### Quiz")
425
+ q1 = gr.Radio(
426
+ ["Exchanges and dark pools route orders differently", "Dark pools set the NBBO", "Dealer markets have no market makers"],
427
+ label="Which statement is true?"
428
  )
429
+ q1_out = gr.Markdown()
430
+ def foundations_q1(ans):
431
+ return "Correct." if ans == "Exchanges and dark pools route orders differently" else "Review: NBBO set by lit venues; dealers make markets."
432
+ q1_btn = gr.Button("Submit")
433
+ q1_btn.click(foundations_q1, q1, q1_out)
434
+
435
+ with gr.TabItem("1.2 Accounts & Orders"):
436
+ gr.Markdown("""
437
+ ### Accounts, Brokers & Order Execution
438
+ - Cash vs margin; PDT basics (US); leverage and borrow fees.
439
+ - Order types: market, limit, stop, stop‑limit, trailing; OCO and bracket orders for structure.
440
+ - Slippage, spreads, liquidity; when L2/time & sales help vs harm.
441
+ """)
442
+
443
+ with gr.TabItem("1.3 Fees & Taxes"):
444
+ gr.Markdown("""
445
+ ### Fees, Taxes & Recordkeeping
446
+ - Commissions vs PFOF; short borrow fees; margin interest and compounding risk.
447
+ - High‑level tax: short vs long‑term gains; wash sale basics.
448
+ - Trade journal: screenshots, tags, metrics for improvement.
449
+ """)
450
+
451
+ with gr.TabItem("1.4 Charts & Data"):
452
+ gr.Markdown("""
453
+ ### Charts, Timeframes & Data
454
+ - Candles, OHLC, volume; multi‑timeframe analysis.
455
+ - Indicators (context): MA, RSI, MACD, ATR, VWAP; know what they measure.
456
+ - Economic calendar, earnings, splits, dividends; why surprises move price.
457
+ """)
458
+
459
+ # Risk & Psychology
460
+ with gr.TabItem("2) Risk & Psychology"):
461
+ with gr.Tabs():
462
+ with gr.TabItem("2.1 Risk Core"):
463
+ gr.Markdown("""
464
+ ### Risk Management Core
465
+ - Risk‑per‑trade sizing from stop distance (ATR/structure).
466
+ - Reward:risk, win rate, expectancy; drawdown controls and circuit breakers.
467
+ """)
468
+ with gr.Row():
469
+ with gr.Column():
470
+ acct = gr.Number(label="Account Equity ($)", value=5000)
471
+ riskpct = gr.Slider(0.1, 5, value=1.0, step=0.1, label="Risk per Trade (%)")
472
+ entry = gr.Number(label="Entry Price", value=100.0)
473
+ stop = gr.Number(label="Stop Price", value=98.0)
474
+ calc_btn = gr.Button("Position Size & Targets")
475
+ with gr.Column():
476
+ rr_out = gr.Textbox(label="Sizing/Targets", lines=6)
477
+ calc_btn.click(rr_position_size_calc, [acct, riskpct, entry, stop], rr_out)
478
+
479
+ gr.Markdown("#### Expectancy Calculator")
480
+ wr = gr.Slider(10, 90, value=45, step=1, label="Win Rate (%)")
481
+ avg_win = gr.Number(label="Avg Win ($)", value=150)
482
+ avg_loss = gr.Number(label="Avg Loss ($)", value=100)
483
+ exp_btn = gr.Button("Compute Expectancy")
484
+ exp_out = gr.Textbox(label="Expectancy", lines=2)
485
+ exp_btn.click(expectancy_calc, [wr, avg_win, avg_loss], exp_out)
486
+
487
+ gr.Markdown("#### Risk of Ruin (Heuristic)")
488
+ wr2 = gr.Slider(10, 90, value=45, step=1, label="Win Rate (%)")
489
+ rr = gr.Slider(0.5, 3.0, value=1.5, step=0.1, label="Reward:Risk")
490
+ bankrisk = gr.Slider(0.5, 10.0, value=1.0, step=0.5, label="Bankroll Risk per Trade (%)")
491
+ ror_btn = gr.Button("Estimate")
492
+ ror_out = gr.Textbox(label="Risk of Ruin", lines=2)
493
+ ror_btn.click(risk_of_ruin_estimator, [wr2, rr, bankrisk], ror_out)
494
+
495
+ with gr.TabItem("2.2 Psychology"):
496
+ gr.Markdown("""
497
+ ### Trader Psychology
498
+ - Biases: loss aversion, FOMO, recency bias; shape environment to reduce impulse.
499
+ - Routines: pre/post‑market checklists, journaling; accountability partners.
500
+ - Written plan + timeouts and trade count limits to curb overtrading.
501
+ """)
502
+
503
+ # Day Trading Track
504
+ with gr.TabItem("3) Day Trading"):
505
+ with gr.Tabs():
506
+ with gr.TabItem("3.1 Overview"):
507
+ gr.Markdown("""
508
+ ### Day Trading Overview
509
+ - Pros: frequent reps/feedback; Cons: noise, slippage, cognitive load.
510
+ - Capital, PDT, margin; ideal markets (liquid large caps/high RVOL).
511
+ """)
512
+ with gr.TabItem("3.2 Intraday Setups"):
513
+ gr.Markdown("""
514
+ ### Intraday Setups
515
+ - ORB & pullbacks; VWAP trends/reversions; momentum ignition; mean‑reversion to PDH/PDL; gap‑fills; news/catalyst trades and halts.
516
+ """)
517
+ gr.Markdown("#### ORB/VWAP Practice")
518
+ gr.Markdown("- Use paper replay: Define hypothesis/trigger/stop/management on selected intraday charts. Visual simulator coming.")
519
+ with gr.TabItem("3.3 Tools & Levels"):
520
+ gr.Markdown("""
521
+ ### Tools & Levels
522
+ - Pre‑market: gap scan, RVOL, news filter.
523
+ - Levels: pre‑market H/L, PDH/PDL, weekly pivots; tape reading when it adds edge.
524
+ """)
525
+ with gr.TabItem("3.4 Execution & Risk"):
526
+ gr.Markdown("""
527
+ ### Execution & Risk
528
+ - Partials, dynamic stops (structure/ATR), hotkeys, brackets, max daily loss, trade count caps.
529
+ """)
530
+ with gr.TabItem("3.5 Playbooks & Cases"):
531
+ gr.Markdown("""
532
+ ### Playbooks & Case Studies
533
+ - Template: hypothesis, trigger, invalidation, targets, management; A+ examples with metrics and screenshots.
534
+ """)
535
+ with gr.TabItem("3.6 Metrics & Review"):
536
+ gr.Markdown("""
537
+ ### Metrics & Review
538
+ - KPIs: expectancy, MAE/MFE, avg hold, adherence; weekly scorecard and top‑3 fixes.
539
+ """)
540
+
541
+ # Swing Trading Track
542
+ with gr.TabItem("4) Swing Trading"):
543
+ with gr.Tabs():
544
+ with gr.TabItem("4.1 Overview"):
545
+ gr.Markdown("""
546
+ ### Swing Trading Overview
547
+ - Pros/cons vs day trading; overnight gap risk; higher signal‑to‑noise via daily/weekly frames.
548
+ """)
549
+ with gr.TabItem("4.2 Setups"):
550
+ gr.Markdown("""
551
+ ### Core Swing Setups
552
+ - Breakouts (base/volatility contraction); pullback to 20/50MA; BOS/higher‑low retests; range trading with ATR stops; earnings season plays.
553
+ """)
554
+ with gr.TabItem("4.3 Scanning"):
555
+ gr.Markdown("""
556
+ ### Scanning & Watchlists
557
+ - Relative strength/weakness vs sector/index; fundamental overlays: EPS growth, margins, debt, sales acceleration.
558
+ - Liquidity and ATR filters to right‑size risk.
559
+ """)
560
+ with gr.TabItem("4.4 Entries/Stops/Targets"):
561
+ gr.Markdown("""
562
+ ### Entries, Stops, and Profit Taking
563
+ - Structural and ATR‑based stops; pyramids/scale; partial profit frameworks; managing gaps/news.
564
+ """)
565
+ with gr.Row():
566
+ with gr.Column():
567
+ entry_s = gr.Number(label="Entry", value=50.0)
568
+ atr_s = gr.Number(label="ATR", value=1.5)
569
+ mult_s = gr.Slider(0.5, 5.0, value=2.0, step=0.5, label="ATR Multiplier")
570
+ side_s = gr.Radio(["Long", "Short"], value="Long", label="Direction")
571
+ atr_btn = gr.Button("Compute Stop")
572
+ with gr.Column():
573
+ atr_out = gr.Textbox(label="Stop Suggestion", lines=2)
574
+ atr_btn.click(atr_stop_calc, [entry_s, atr_s, mult_s, side_s], atr_out)
575
+ with gr.TabItem("4.5 Portfolio & Risk"):
576
+ gr.Markdown("""
577
+ ### Portfolio & Risk
578
+ - Correlation/sector exposure; max names; volatility budgeting; optional options for risk shaping.
579
+ """)
580
+ with gr.TabItem("4.6 Review Cycle"):
581
+ gr.Markdown("""
582
+ ### Review Cycle
583
+ - Weekly prep (Sunday), mid‑week check‑ins; journaling and refinement.
584
+ """)
585
+
586
+ # Long-Term Investing Track
587
+ with gr.TabItem("5) Long‑Term"):
588
+ with gr.Tabs():
589
+ with gr.TabItem("5.1 Foundations"):
590
+ gr.Markdown("""
591
+ ### Investing Foundations
592
+ - Time horizon and risk capacity vs tolerance; DCA vs lump sum; sequence‑of‑returns risk.
593
+ """)
594
+ with gr.TabItem("5.2 Allocation & Diversification"):
595
+ gr.Markdown("""
596
+ ### Asset Allocation & Diversification
597
+ - Core indexing (total market + international + bonds); factor tilts (value/small/quality/momentum); rebalancing rules.
598
+ """)
599
+ with gr.TabItem("5.3 Equity Selection (Optional)"):
600
+ gr.Markdown("""
601
+ ### Equity Selection (Optional)
602
+ - Business quality: moats, ROIC, FCF, balance sheet; valuation snapshots (PE, EV/EBITDA, DCF intuition); dividend growth strategies.
603
+ """)
604
+ with gr.TabItem("5.4 Behavior & Discipline"):
605
+ gr.Markdown("""
606
+ ### Behavior & Discipline
607
+ - Avoid panic buying/selling; automate contributions; write an IPS (Investment Policy Statement).
608
+ """)
609
+ with gr.TabItem("5.5 Tax Optimization"):
610
+ gr.Markdown("""
611
+ ### Tax Optimization (High level)
612
+ - Accounts: taxable vs tax‑advantaged; asset location basics; harvest concepts. Consult a tax professional.
613
+ """)
614
+
615
+ # Validation & Development
616
+ with gr.TabItem("6) Validation"):
617
+ with gr.Tabs():
618
+ with gr.TabItem("6.1 Back/Forward Testing"):
619
+ gr.Markdown("""
620
+ ### Backtesting & Forward Testing
621
+ - Data quality, survivorship/look‑ahead bias; walk‑forward, out‑of‑sample validation.
622
+ """)
623
+ with gr.TabItem("6.2 Paper → Capital"):
624
+ gr.Markdown("""
625
+ ### Paper Trading & Phased Deployment
626
+ - Simulator → micro size → scale by adherence/expectancy KPIs.
627
+ """)
628
+ with gr.TabItem("6.3 KPIs & Edge"):
629
+ gr.Markdown("""
630
+ ### KPIs & Edge Tracking
631
+ - Define edge; maintain playbooks; use an expectancy/KPI dashboard.
632
+ """)
633
+
634
+ # Compliance & Safety
635
+ with gr.TabItem("7) Compliance"):
636
+ gr.Markdown("""
637
+ ### Compliance, Ethics & Safety
638
+ - PDT (US) overview; margin basics; short‑selling mechanics and borrow fees; Reg SHO context.
639
+ - Earnings and MNPI; avoid rumors/pumps; safeguards: max daily loss, time‑outs, risk limits.
640
+ """)
641
+
642
+ # Capstones
643
+ with gr.TabItem("8) Capstones"):
644
+ gr.Markdown("""
645
+ ### Capstones & Certifications
646
+ - Day: 20 simulated trades across 3 setups with predefined risk and full journals.
647
+ - Swing: manage a 5‑name swing portfolio for 6 simulated weeks.
648
+ - Long‑Term: craft an IPS and backtest a DCA plan through historical drawdowns.
649
+ """)
650
+
651
+ # App-Native Components
652
+ with gr.TabItem("9) App‑Native"):
653
+ gr.Markdown("""
654
+ ### App‑Native Components
655
+ - Calculators: position size, expectancy, ATR stops, DCA, rebalancing.
656
+ - Simulators: intraday replay, gap/open auction, earnings reaction.
657
+ - Checklists: pre‑market, weekly swing review, quarterly IPS.
658
+ - Dashboards: KPIs (win rate, RR, expectancy, equity curve), risk heatmap.
659
+ - Journaling: tags, screenshots, reasons to enter/exit, emotions.
660
+ """)
661
 
662
  gr.Markdown("""
663
  ---
664
+ Data: Polygon (quotes), Finnhub (financials), Yahoo RSS (news), SEC EDGAR (filings), TradingView (charts).
665
+ Troubleshooting: Check tickers, API keys, and retry if rate-limited.
 
666
  """)
667
 
668
  ticker_input.change(
 
673
  refresh_btn.click(
674
  fn=update_stock_info,
675
  inputs=[ticker_input],
676
+ outputs=[quote_output, news_output, filings_output, chart_output]
677
  )
678
 
679
  if __name__ == "__main__":