mr601s commited on
Commit
59883fb
Β·
verified Β·
1 Parent(s): 4875162

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -437
app.py CHANGED
@@ -1,11 +1,3 @@
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,15 +8,11 @@ import random
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,89 +20,84 @@ def fetch_polygon_quote(ticker, polygon_api_key=POLYGON_API_KEY):
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:
45
  return f"❌ Error: {str(e)}"
46
 
 
47
  def get_financial_summary_finnhub(ticker, finnhub_api_key=FINNHUB_API_KEY):
48
  url = f"https://finnhub.io/api/v1/stock/metric?symbol={ticker.upper()}&metric=all&token={finnhub_api_key}"
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,22 +116,19 @@ class SECUtils:
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,32 +139,35 @@ class NewsUtils:
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":
@@ -200,8 +183,8 @@ def simulate_order_book(side, order_type, price, size, seed=123):
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,8 +194,8 @@ def simulate_order_book(side, order_type, price, size, seed=123):
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)
@@ -221,91 +204,37 @@ def simulate_order_book(side, order_type, price, size, seed=123):
221
  fill_msg = f"Limit sell posted above book: {price:.2f}. Not filled."
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,24 +254,27 @@ def update_stock_info(ticker):
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,10 +283,8 @@ Honest, educational, and data‑rich. Use quotes, news, filings, and charts alon
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,304 +297,60 @@ Honest, educational, and data‑rich. Use quotes, news, filings, and charts alon
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(
669
  fn=update_stock_info,
670
  inputs=[ticker_input],
@@ -673,7 +359,7 @@ Troubleshooting: Check tickers, API keys, and retry if rate-limited.
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__":
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import requests
3
  import numpy as np
 
8
  from datetime import datetime
9
  import os
10
 
11
+ # === CONFIG: API KEYS ===
 
 
12
  POLYGON_API_KEY = os.getenv("POLYGON_API_KEY") or "fAhg47wPlf4FT6U2Hn23kQoQCQIyW0G_"
13
+ FINNHUB_API_KEY = "d2urs69r01qq994h1f5gd2urs69r01qq994h1f60"
14
 
15
+ # === QUOTE SECTION ===
 
 
16
  def fetch_polygon_quote(ticker, polygon_api_key=POLYGON_API_KEY):
17
  url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.upper()}/prev?adjusted=true&apiKey={polygon_api_key}"
18
  try:
 
20
  response.raise_for_status()
21
  data = response.json()
22
  if data.get("results"):
23
+ last = data["results"][0]
24
+ price = last["c"]
25
+ close_dt = datetime.utcfromtimestamp(last["t"] / 1000).strftime('%Y-%m-%d')
26
+ 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)_"
 
 
 
27
  else:
28
  return f"❌ Quote data unavailable for {ticker.upper()}."
29
  except Exception as e:
30
  return f"❌ Error: {str(e)}"
31
 
32
+ # === FINNHUB FINANCIAL SUMMARY ===
33
  def get_financial_summary_finnhub(ticker, finnhub_api_key=FINNHUB_API_KEY):
34
  url = f"https://finnhub.io/api/v1/stock/metric?symbol={ticker.upper()}&metric=all&token={finnhub_api_key}"
35
  try:
36
  response = requests.get(url, timeout=10)
37
  response.raise_for_status()
38
+ data = response.json()
39
+ metrics = data.get('metric', {})
40
  if not metrics:
41
  return f"πŸ“Š **Financial Summary for {ticker.upper()}**\n\n❌ No financial data found."
42
  result = f"πŸ“Š **Financial Summary for {ticker.upper()}**\n\n"
43
+ if metrics.get('totalRevenueTTM'):
44
  result += f"β€’ **Revenue (TTM):** ${int(metrics['totalRevenueTTM']):,}\n"
45
+ if metrics.get('netIncomeTTM'):
46
  result += f"β€’ **Net Income (TTM):** ${int(metrics['netIncomeTTM']):,}\n"
47
  pe = metrics.get('peNormalizedAnnual') or metrics.get('peExclExtraTTM')
48
  if pe is not None:
49
+ result += f"β€’ **P/E Ratio:** {pe:.2f}\n"
50
  pb = metrics.get('pbAnnual')
51
  if pb is not None:
52
+ result += f"β€’ **P/B Ratio:** {pb:.2f}\n"
53
  dy = metrics.get('dividendYieldIndicatedAnnual')
54
  if dy is not None:
55
+ result += f"β€’ **Dividend Yield:** {dy:.2f}%\n"
56
  dte = metrics.get('totalDebt/totalEquityAnnual')
57
  if dte is not None:
58
+ result += f"β€’ **Debt/Equity:** {dte:.2f}\n"
59
  pm = metrics.get('netProfitMarginTTM')
60
  if pm is not None:
61
+ result += f"β€’ **Net Profit Margin:** {pm:.2f}%\n"
62
  mc = metrics.get('marketCapitalization')
63
  if mc is not None:
64
  result += f"β€’ **Market Cap:** ${int(mc):,}\n"
65
  if result.strip() == f"πŸ“Š **Financial Summary for {ticker.upper()}**":
66
+ return f"πŸ“Š **Financial Summary for {ticker.upper()}**\n\n❌ No data available from Finnhub."
67
  return result
68
  except Exception as e:
69
  return f"πŸ“Š **Financial Summary for {ticker.upper()}**\n\n❌ Error fetching financial summary: {e}"
70
 
71
+ # === SEC Utilities ===
 
 
72
  class SECUtils:
73
  def __init__(self):
74
  self.cik_lookup_url = "https://www.sec.gov/files/company_tickers.json"
75
  self.edgar_search_url = "https://data.sec.gov/submissions/CIK{cik}.json"
76
+ self.headers = {"User-Agent": "StockResearchMVP/1.0 (educational@example.com)"}
 
77
  def get_cik(self, ticker):
78
  try:
79
  time.sleep(0.5)
80
  response = requests.get(self.cik_lookup_url, headers=self.headers, timeout=20)
81
+ if response.status_code != 200:
82
+ return None
83
  data = response.json()
84
+ for k, v in data.items():
85
  if isinstance(v, dict) and v.get('ticker', '').upper() == ticker.upper():
86
  return str(v['cik_str']).zfill(10)
87
  return None
88
  except Exception as e:
89
  print(f"CIK lookup error: {e}")
90
  return None
 
91
  def get_recent_filings(self, ticker):
92
  try:
93
  cik = self.get_cik(ticker)
94
  if not cik:
95
+ 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."
96
  time.sleep(0.5)
97
  url = self.edgar_search_url.format(cik=cik)
98
  response = requests.get(url, headers=self.headers, timeout=20)
99
  if response.status_code != 200:
100
+ 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."
101
  data = response.json()
102
  filings = data.get('filings', {}).get('recent', {})
103
  if not filings or not filings.get('form'):
 
116
  result += f" πŸ“Ž [View Filing]({filing_url})\n\n"
117
  return result
118
  except Exception as e:
119
+ 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."
120
 
121
+ # === News Utilities ===
 
 
122
  class NewsUtils:
123
  def __init__(self):
124
+ self.headers = {"User-Agent": "StockResearchMVP/1.0 (educational@example.com)"}
 
125
  def get_yahoo_news(self, ticker):
126
  try:
127
  time.sleep(random.uniform(0.5, 1.0))
128
  url = f"https://feeds.finance.yahoo.com/rss/2.0/headline?s={ticker}&region=US&lang=en-US"
129
  feed = feedparser.parse(url)
130
  if not feed.entries:
131
+ 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})"
132
  result = f"πŸ“° **Latest News for {ticker}**\n\n"
133
  for i, entry in enumerate(feed.entries[:5]):
134
  title = getattr(entry, 'title', 'No title')
 
139
  result += f" πŸ”— [Read More]({link})\n\n"
140
  return result
141
  except Exception as e:
142
+ 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})"
143
 
144
+ # === TradingView Widget Chart Embed ===
 
 
145
  def get_tradingview_embed(ticker):
146
+ ticker = ticker.strip().upper() if ticker else "AAPL"
147
  ticker = ''.join(filter(str.isalnum, ticker))
148
+ return f"""
149
+ <iframe src="https://s.tradingview.com/widgetembed/?symbol={ticker}&interval=D&hidesidetoolbar=1&theme=light"
150
+ width="100%" height="400" frameborder="0" allowtransparency="true" scrolling="no"></iframe>
151
+ """
152
+
153
+ # === LESSON MODULES ===
154
 
155
+ # 1. Order Book Simulator
 
 
156
  def simulate_order_book(side, order_type, price, size, seed=123):
157
+ np.random.seed(seed)
158
  base_price = 100.00
159
  levels = np.arange(base_price - 2, base_price + 2.5, 0.5)
160
  buy_sizes = np.random.randint(1, 40, len(levels))
161
  sell_sizes = np.random.randint(1, 40, len(levels))
 
162
  buy_mask = levels < base_price
163
  sell_mask = levels > base_price
164
  buys = np.where(buy_mask, buy_sizes, 0)
165
  sells = np.where(sell_mask, sell_sizes, 0)
166
+ df = pd.DataFrame({
167
+ 'Price': levels,
168
+ 'Buy Size': buys,
169
+ 'Sell Size': sells
170
+ }).sort_values(by='Price', ascending=False).reset_index(drop=True)
171
 
172
  fill_msg = ""
173
  if order_type == "Market":
 
183
  if side == "Buy":
184
  if price >= df['Price'].min():
185
  sells_at_or_below = df[(df['Price'] <= price) & (df['Sell Size'] > 0)]
186
+ if sells_at_or_below.shape[0]:
187
+ fill_price = sells_at_or_below.iloc[0]['Price']
188
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Buy)"
189
  else:
190
  queue_spot = 1 + np.random.randint(0, 3)
 
194
  else:
195
  if price <= df['Price'].max():
196
  buys_at_or_above = df[(df['Price'] >= price) & (df['Buy Size'] > 0)]
197
+ if buys_at_or_above.shape[0]:
198
+ fill_price = buys_at_or_above.iloc[0]['Price']
199
  fill_msg = f"Filled {size} @ {fill_price:.2f} (Aggressive Limit Sell)"
200
  else:
201
  queue_spot = 1 + np.random.randint(0, 3)
 
204
  fill_msg = f"Limit sell posted above book: {price:.2f}. Not filled."
205
  return df, fill_msg
206
 
207
+ # 2. Slippage Estimator
208
  def slippage_estimator(side, order_size, seed=123):
209
+ np.random.seed(seed)
210
  base_price = 100
211
+ levels = np.arange(base_price-2, base_price+2.5, 0.5)
 
212
  if side == "Buy":
213
+ sizes = np.random.randint(10, 70, len(levels))
214
+ prices = levels[levels > base_price]
215
+ sizes = sizes[levels > base_price]
 
216
  else:
217
+ sizes = np.random.randint(10, 70, len(levels))
218
+ prices = levels[levels < base_price]
219
+ sizes = sizes[levels < base_price]
220
+ remaining = order_size
 
 
 
 
 
221
  fills = []
222
  for p, s in zip(prices, sizes):
223
+ take = min(s, remaining)
224
+ fills.append((p, take))
225
+ remaining -= take
 
226
  if remaining <= 0:
227
  break
 
228
  if remaining > 0:
229
  return "Not enough liquidity to fill order!", None
 
230
  df = pd.DataFrame(fills, columns=["Price", "Shares"])
231
+ avg_fill = (df["Price"] * df["Shares"]).sum() / order_size
232
  slip = avg_fill - base_price if side == "Buy" else base_price - avg_fill
233
  slip_pct = (slip / base_price) * 100
234
  summary = f"Est. avg fill @ {avg_fill:.2f}; Slippage: {slip:.2f} ({slip_pct:.2f}%) from ideal {base_price}"
235
  return summary, df
236
 
237
+ # === Instantiate Utilities ===
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  sec_utils = SECUtils()
239
  news_utils = NewsUtils()
240
 
 
254
  chart_html = get_tradingview_embed(ticker)
255
  return quote_data, news_data, filings_data, financial_data, chart_html
256
 
 
 
 
257
  css = """
258
+ .gradio-container {
259
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
260
+ max-width: 1400px;
261
+ margin: 0 auto;
262
+ }
263
+ .tab-nav button {
264
+ font-size: 16px;
265
+ font-weight: 600;
266
+ }
267
  """
268
 
269
+ with gr.Blocks(css=css, theme=gr.themes.Soft(), title="Stock Research Platform") as demo:
 
 
270
  gr.Markdown("""
271
+ # πŸ“ˆ Stock Research Platform MVP
272
+ **Comprehensive stock analysis, real-time data, and interactive education modules.**
273
 
274
+ 🎯 Enter a stock ticker symbol (**AAPL**, **TSLA**, **MSFT**, **GOOGL**) for market data, or check out the Lessons tab for learning modules!
 
275
 
276
+ ⚠️ **Note**: Stock quote data from Polygon (Previous Close for free plans). Financial summary from Finnhub. Charts powered by TradingView.
277
+ """)
278
  with gr.Row():
279
  with gr.Column(scale=3):
280
  ticker_input = gr.Textbox(
 
283
  value="AAPL"
284
  )
285
  with gr.Column(scale=1):
286
+ refresh_btn = gr.Button("πŸ”„ Refresh Data", variant="primary", size="lg")
 
287
  with gr.Tabs():
 
288
  with gr.TabItem("πŸ’° Quote & Overview"):
289
  quote_output = gr.Markdown(value="Enter a ticker to see stock quote")
290
  with gr.TabItem("πŸ“° News"):
 
297
  gr.Markdown("### Interactive Price Chart")
298
  gr.Markdown("*Powered by TradingView*")
299
  chart_output = gr.HTML(get_tradingview_embed("AAPL"))
300
+ with gr.TabItem("πŸŽ“ Lessons"):
 
 
301
  with gr.Tabs():
302
+ with gr.TabItem("Lesson 1: Exchanges & Order Book"):
 
303
  gr.Markdown("""
304
+ ## Lesson 1: Exchanges, dark pools, auction vs. dealer markets
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
 
306
+ - *[Paste your main lesson text here, or sync from Google Doc]*
 
 
 
 
 
 
307
 
308
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ **Explore:** Try the tools below to visualize real order book mechanics and slippage.
 
 
 
 
 
 
 
 
 
 
 
311
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  with gr.Tabs():
313
+ with gr.TabItem("Order Book Simulator"):
314
+ lesson1_order = gr.Interface(
315
+ fn=simulate_order_book,
316
+ inputs=[
317
+ gr.Dropdown(["Buy", "Sell"], label="Order Side"),
318
+ gr.Dropdown(["Market", "Limit"], label="Order Type"),
319
+ gr.Number(value=100.00, label="Order Price (for limit)"),
320
+ gr.Slider(1, 100, value=10, step=1, label="Order Size"),
321
+ gr.Number(value=123, label="Seed (optional, for replay)"),
322
+ ],
323
+ outputs=[
324
+ gr.Dataframe(label="Order Book (randomized)"),
325
+ gr.Textbox(label="Result / Fill Message"),
326
+ ],
327
+ live=False,
328
+ allow_flagging="never"
329
+ )
330
+ with gr.TabItem("Slippage Estimator"):
331
+ lesson1_slippage = gr.Interface(
332
+ fn=slippage_estimator,
333
+ inputs=[
334
+ gr.Dropdown(["Buy", "Sell"], label="Order Side"),
335
+ gr.Slider(1, 300, value=50, step=1, label="Order Size"),
336
+ gr.Number(value=123, label="Seed (for repeatability)"),
337
+ ],
338
+ outputs=[
339
+ gr.Textbox(label="Estimate"),
340
+ gr.Dataframe(label="Fill breakdown"),
341
+ ],
342
+ live=False,
343
+ allow_flagging="never"
344
+ )
345
 
346
+ # Add more lessons as new gr.TabItem("Lesson X: ...") blocks here
 
 
 
 
 
 
 
 
 
347
 
348
  gr.Markdown("""
349
  ---
350
+ **Data Sources:** Polygon.io for quotes, Finnhub for financials, Yahoo RSS for news, SEC EDGAR for filings.
 
 
351
 
352
+ **Troubleshooting:** If you encounter errors, double-check your ticker or wait and retry.
353
+ """)
354
  ticker_input.change(
355
  fn=update_stock_info,
356
  inputs=[ticker_input],
 
359
  refresh_btn.click(
360
  fn=update_stock_info,
361
  inputs=[ticker_input],
362
+ outputs=[quote_output, news_output, filings_output, financial_output, chart_output]
363
  )
364
 
365
  if __name__ == "__main__":