Tulitula commited on
Commit
3cb3ebf
·
verified ·
1 Parent(s): 653d088

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -131
app.py CHANGED
@@ -392,19 +392,21 @@ def set_horizon(years: float):
392
  def search_tickers_cb(q: str):
393
  opts = yahoo_search(q)
394
  if not opts:
395
- opts = ["No matches found"]
396
- # Put helper text into Matches box
 
 
 
 
397
  return gr.update(
398
  choices=opts,
399
- value=None,
400
  info="Select a symbol and click 'Add selected to portfolio'."
401
  )
402
 
403
  def add_symbol(selection: str, table: Optional[pd.DataFrame]):
404
- if not selection:
405
- return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "Pick a row in Matches first."
406
- if "No matches" in str(selection):
407
- return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "No symbol to add."
408
  symbol = selection.split("|")[0].strip().upper()
409
 
410
  current = []
@@ -429,8 +431,7 @@ def add_symbol(selection: str, table: Optional[pd.DataFrame]):
429
  return new_table, f"Added {symbol}."
430
 
431
  def add_symbol_table_only(selection: str, table: Optional[pd.DataFrame]):
432
- """Wrapper that only returns the updated table (no status string)."""
433
- new_table, _ = add_symbol(selection, table)
434
  return new_table
435
 
436
  def lock_ticker_column(tb: Optional[pd.DataFrame]):
@@ -460,11 +461,9 @@ def compute(
460
  years_lookback: int,
461
  table: Optional[pd.DataFrame],
462
  pick_band_to_show: str, # "Low" | "Medium" | "High"
463
- progress=gr.Progress(track_tqdm=True)
464
  ):
465
- # progress indicator
466
- progress(0, desc="Validating inputs...")
467
- time.sleep(0.05)
468
 
469
  # sanitize table
470
  if isinstance(table, pd.DataFrame):
@@ -479,35 +478,44 @@ def compute(
479
 
480
  symbols = [t for t in df["ticker"].tolist() if t]
481
  if len(symbols) == 0:
482
- msg = "Add at least one ticker (and ensure VOO is present in data history)."
 
 
 
483
  return (
484
- gr.update(value=None, visible=True), # plot placeholder stays blank
485
- gr.update(value="### Error\n" + msg, visible=True),
486
- gr.update(value=empty_positions_df(), visible=True),
487
- gr.update(value=empty_suggestion_df(), visible=True),
488
- gr.update(value=None, visible=False),
489
- gr.update(value="", visible=True),
490
- gr.update(value="", visible=True),
491
- gr.update(value="", visible=True),
492
- gr.update(visible=True), # show outputs group so message is visible
493
- gr.update(visible=True) # show suggestions container (buttons/text)
 
 
494
  )
495
 
496
- progress(0.15, desc="Validating tickers...")
497
  symbols = validate_tickers(symbols, years_lookback)
498
  if len(symbols) == 0:
499
- msg = "Could not validate any tickers for the chosen lookback (VOO data required)."
 
 
 
500
  return (
501
- gr.update(value=None, visible=True),
502
- gr.update(value="### Error\n" + msg, visible=True),
503
- gr.update(value=empty_positions_df(), visible=True),
504
- gr.update(value=empty_suggestion_df(), visible=True),
505
- gr.update(value=None, visible=False),
506
- gr.update(value="", visible=True),
507
- gr.update(value="", visible=True),
508
- gr.update(value="", visible=True),
 
 
 
509
  gr.update(visible=True),
510
- gr.update(visible=True)
511
  )
512
 
513
  global UNIVERSE
@@ -517,37 +525,40 @@ def compute(
517
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
518
  rf_ann = RF_ANN
519
 
520
- progress(0.35, desc="Downloading prices & estimating moments...")
521
  moms = estimate_all_moments_aligned(symbols, years_lookback, rf_ann)
522
  betas, covA, erp_ann, sigma_mkt = moms["betas"], moms["cov_ann"], moms["erp_ann"], moms["sigma_m_ann"]
523
 
524
  gross = sum(abs(v) for v in amounts.values())
525
  if gross <= 1e-12:
526
- msg = "All amounts are zero."
 
 
 
527
  return (
528
- gr.update(value=None, visible=True),
529
- gr.update(value="### Error\n" + msg, visible=True),
530
- gr.update(value=empty_positions_df(), visible=True),
531
- gr.update(value=empty_suggestion_df(), visible=True),
532
- gr.update(value=None, visible=False),
533
- gr.update(value="", visible=True),
534
- gr.update(value="", visible=True),
535
- gr.update(value="", visible=True),
 
 
 
536
  gr.update(visible=True),
537
- gr.update(visible=True)
538
  )
539
  weights = {k: v / gross for k, v in amounts.items()}
540
 
541
- # Portfolio CAPM stats
542
- progress(0.55, desc="Computing CAPM stats...")
543
  beta_p, mu_capm, sigma_hist = portfolio_stats(weights, covA, betas, rf_ann, erp_ann)
544
 
545
- # Efficient alternatives on CML
546
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
547
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
548
 
549
- # Synthetic dataset & suggestions — exactly the user's tickers
550
- progress(0.75, desc="Building synthetic dataset & suggestions...")
551
  user_universe = list(symbols)
552
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
553
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
@@ -556,6 +567,7 @@ def compute(
556
  except Exception:
557
  csv_path = None
558
 
 
559
  picks = suggest_one_per_band(synth, sigma_mkt, user_universe)
560
 
561
  def _fmt(row: pd.Series) -> str:
@@ -572,11 +584,11 @@ def compute(
572
  if chosen is None or chosen.empty:
573
  chosen_sigma = None
574
  chosen_mu = None
575
- sugg_table_df = empty_suggestion_df()
576
  else:
577
  chosen_sigma = float(chosen["sigma_hist"])
578
  chosen_mu = float(chosen["mu_capm"])
579
- sugg_table_df = _holdings_table_from_row(chosen, budget=gross)
580
 
581
  pos_table = pd.DataFrame(
582
  [{
@@ -588,6 +600,7 @@ def compute(
588
  columns=["ticker", "amount_usd", "weight_exposure", "beta"]
589
  )
590
 
 
591
  img = plot_cml(
592
  rf_ann, erp_ann, sigma_mkt,
593
  sigma_hist, mu_capm,
@@ -595,7 +608,6 @@ def compute(
595
  sugg_sigma_hist=chosen_sigma, sugg_mu_capm=chosen_mu
596
  )
597
 
598
- # Summary text (clean)
599
  info = "\n".join([
600
  "### Inputs",
601
  f"- Lookback years {years_lookback}",
@@ -617,87 +629,84 @@ def compute(
617
  "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
618
  ])
619
 
620
- progress(0.98, desc="Rendering...")
621
- time.sleep(0.05)
622
-
623
  return (
624
- gr.update(value=img, visible=True),
625
- gr.update(value=info, visible=True),
626
- gr.update(value=pos_table, visible=True),
627
- gr.update(value=sugg_table_df, visible=True),
628
- gr.update(value=csv_path, visible=bool(csv_path)),
629
- gr.update(value=txt_low, visible=True),
630
- gr.update(value=txt_med, visible=True),
631
- gr.update(value=txt_high, visible=True),
632
- gr.update(visible=True), # show outputs group
633
- gr.update(visible=True) # show suggestions group
 
 
634
  )
635
 
636
  # -------------- UI --------------
637
- with gr.Blocks(title="Efficient Portfolio Advisor") as demo:
638
  gr.Markdown(
639
  "## Efficient Portfolio Advisor\n"
640
- "Search symbols, enter **dollar amounts**, set horizon. Returns use Yahoo Finance monthly data; risk-free from FRED. "
641
- "Plot shows **your CAPM point on the CML** plus efficient market/bills points."
642
  )
643
 
644
- # LEFT: controls (fill page initially)
645
- with gr.Group(visible=True) as controls_group:
646
- # Search flow: Search -> Button -> Matches -> Add
647
- q = gr.Textbox(label="Search symbol")
648
- search_btn = gr.Button("Search")
649
- matches = gr.Dropdown(choices=[], label="Matches")
650
- add_btn = gr.Button("Add selected to portfolio")
651
-
652
- gr.Markdown("### Portfolio positions")
653
- table = gr.Dataframe(
654
- headers=["ticker", "amount_usd"],
655
- datatype=["str", "number"],
656
- row_count=0,
657
- col_count=(2, "fixed")
658
- )
 
659
 
660
- horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
661
- lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
662
 
663
- # Compute button directly under lookback slider
664
- run_btn = gr.Button("Compute (build dataset & suggest)")
665
 
666
- # Suggestions container (hidden until compute)
667
- with gr.Group(visible=False) as sugg_group:
668
- gr.Markdown("### Suggestions")
669
  with gr.Row():
670
- btn_low = gr.Button("Show Low")
671
- btn_med = gr.Button("Show Medium")
672
- btn_high = gr.Button("Show High")
673
- low_txt = gr.Markdown()
674
- med_txt = gr.Markdown()
675
- high_txt = gr.Markdown()
676
-
677
- # RIGHT: outputs (hidden until compute)
678
- with gr.Group(visible=False) as out_group:
679
- with gr.Row():
680
- with gr.Column(scale=1):
681
- plot = gr.Image(label="Capital Market Line (CAPM)", type="pil")
682
- summary = gr.Markdown(label="Inputs & Results")
683
- with gr.Column(scale=1):
684
- positions = gr.Dataframe(
685
- label="Computed positions",
686
- headers=["ticker", "amount_usd", "weight_exposure", "beta"],
687
- datatype=["str", "number", "number", "number"],
688
- col_count=(4, "fixed"),
689
- value=empty_positions_df(),
690
- interactive=False
691
- )
692
- sugg_table = gr.Dataframe(
693
- label="Selected suggestion holdings (% / $)",
694
- headers=["ticker", "weight_%", "amount_$"],
695
- datatype=["str", "number", "number"],
696
- col_count=(3, "fixed"),
697
- value=empty_suggestion_df(),
698
- interactive=False
699
- )
700
- dl = gr.File(label="Generated dataset CSV", value=None, visible=True)
701
 
702
  # wire search / add / locking
703
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=matches)
@@ -707,25 +716,25 @@ with gr.Blocks(title="Efficient Portfolio Advisor") as demo:
707
  # horizon updates globals silently (no UI output)
708
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
709
 
710
- # compute + render (default to Medium band)
711
  run_btn.click(
712
  fn=compute,
713
  inputs=[lookback, table, gr.State("Medium")],
714
  outputs=[
715
  plot, summary, positions, sugg_table, dl,
716
  low_txt, med_txt, high_txt,
717
- out_group, sugg_group
718
  ]
719
  )
720
 
721
- # band buttons recompute picks quickly (keep groups visible)
722
  btn_low.click(
723
  fn=compute,
724
  inputs=[lookback, table, gr.State("Low")],
725
  outputs=[
726
  plot, summary, positions, sugg_table, dl,
727
  low_txt, med_txt, high_txt,
728
- out_group, sugg_group
729
  ]
730
  )
731
  btn_med.click(
@@ -734,7 +743,7 @@ with gr.Blocks(title="Efficient Portfolio Advisor") as demo:
734
  outputs=[
735
  plot, summary, positions, sugg_table, dl,
736
  low_txt, med_txt, high_txt,
737
- out_group, sugg_group
738
  ]
739
  )
740
  btn_high.click(
@@ -743,7 +752,7 @@ with gr.Blocks(title="Efficient Portfolio Advisor") as demo:
743
  outputs=[
744
  plot, summary, positions, sugg_table, dl,
745
  low_txt, med_txt, high_txt,
746
- out_group, sugg_group
747
  ]
748
  )
749
 
@@ -752,6 +761,4 @@ RF_CODE = fred_series_for_horizon(HORIZON_YEARS)
752
  RF_ANN = fetch_fred_yield_annual(RF_CODE)
753
 
754
  if __name__ == "__main__":
755
- # enable queue for visible progress bar
756
  demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)
757
-
 
392
  def search_tickers_cb(q: str):
393
  opts = yahoo_search(q)
394
  if not opts:
395
+ return gr.update(
396
+ choices=["No matches found"],
397
+ value=None,
398
+ info="No matches."
399
+ )
400
+ first = opts[0] # preselect the first hit
401
  return gr.update(
402
  choices=opts,
403
+ value=first,
404
  info="Select a symbol and click 'Add selected to portfolio'."
405
  )
406
 
407
  def add_symbol(selection: str, table: Optional[pd.DataFrame]):
408
+ if (not selection) or ("No matches" in selection):
409
+ return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "Pick a valid match first."
 
 
410
  symbol = selection.split("|")[0].strip().upper()
411
 
412
  current = []
 
431
  return new_table, f"Added {symbol}."
432
 
433
  def add_symbol_table_only(selection: str, table: Optional[pd.DataFrame]):
434
+ new_table, _msg = add_symbol(selection, table)
 
435
  return new_table
436
 
437
  def lock_ticker_column(tb: Optional[pd.DataFrame]):
 
461
  years_lookback: int,
462
  table: Optional[pd.DataFrame],
463
  pick_band_to_show: str, # "Low" | "Medium" | "High"
464
+ progress=gr.Progress(track_tqdm=True),
465
  ):
466
+ progress(0.05, desc="Validating tickers...")
 
 
467
 
468
  # sanitize table
469
  if isinstance(table, pd.DataFrame):
 
478
 
479
  symbols = [t for t in df["ticker"].tolist() if t]
480
  if len(symbols) == 0:
481
+ out_empty = gr.update(visible=True, value="Add at least one ticker.")
482
+ empty_df = gr.update(visible=True, value=empty_positions_df())
483
+ empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
484
+ none_file = gr.update(visible=True, value=None)
485
  return (
486
+ gr.update(visible=True, value=None), # plot
487
+ out_empty, # summary
488
+ empty_df, # positions
489
+ empty_sugg, # sugg_table
490
+ none_file, # file
491
+ gr.update(visible=True, value=""), # low_txt
492
+ gr.update(visible=True, value=""), # med_txt
493
+ gr.update(visible=True, value=""), # high_txt
494
+ gr.update(visible=True), # md_sugg
495
+ gr.update(visible=True), # btn_low
496
+ gr.update(visible=True), # btn_med
497
+ gr.update(visible=True), # btn_high
498
  )
499
 
 
500
  symbols = validate_tickers(symbols, years_lookback)
501
  if len(symbols) == 0:
502
+ out_empty = gr.update(visible=True, value="Could not validate any tickers.")
503
+ empty_df = gr.update(visible=True, value=empty_positions_df())
504
+ empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
505
+ none_file = gr.update(visible=True, value=None)
506
  return (
507
+ gr.update(visible=True, value=None),
508
+ out_empty,
509
+ empty_df,
510
+ empty_sugg,
511
+ none_file,
512
+ gr.update(visible=True, value=""),
513
+ gr.update(visible=True, value=""),
514
+ gr.update(visible=True, value=""),
515
+ gr.update(visible=True),
516
+ gr.update(visible=True),
517
+ gr.update(visible=True),
518
  gr.update(visible=True),
 
519
  )
520
 
521
  global UNIVERSE
 
525
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
526
  rf_ann = RF_ANN
527
 
528
+ progress(0.20, desc="Downloading prices & computing returns...")
529
  moms = estimate_all_moments_aligned(symbols, years_lookback, rf_ann)
530
  betas, covA, erp_ann, sigma_mkt = moms["betas"], moms["cov_ann"], moms["erp_ann"], moms["sigma_m_ann"]
531
 
532
  gross = sum(abs(v) for v in amounts.values())
533
  if gross <= 1e-12:
534
+ out_empty = gr.update(visible=True, value="All amounts are zero.")
535
+ empty_df = gr.update(visible=True, value=empty_positions_df())
536
+ empty_sugg = gr.update(visible=True, value=empty_suggestion_df())
537
+ none_file = gr.update(visible=True, value=None)
538
  return (
539
+ gr.update(visible=True, value=None),
540
+ out_empty,
541
+ empty_df,
542
+ empty_sugg,
543
+ none_file,
544
+ gr.update(visible=True, value=""),
545
+ gr.update(visible=True, value=""),
546
+ gr.update(visible=True, value=""),
547
+ gr.update(visible=True),
548
+ gr.update(visible=True),
549
+ gr.update(visible=True),
550
  gr.update(visible=True),
 
551
  )
552
  weights = {k: v / gross for k, v in amounts.items()}
553
 
554
+ progress(0.35, desc="Computing CAPM stats...")
 
555
  beta_p, mu_capm, sigma_hist = portfolio_stats(weights, covA, betas, rf_ann, erp_ann)
556
 
557
+ progress(0.50, desc="Efficient mixes on CML...")
558
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
559
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
560
 
561
+ progress(0.70, desc="Building 1,000 candidate mixes...")
 
562
  user_universe = list(symbols)
563
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
564
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
 
567
  except Exception:
568
  csv_path = None
569
 
570
+ progress(0.85, desc="Ranking suggestions...")
571
  picks = suggest_one_per_band(synth, sigma_mkt, user_universe)
572
 
573
  def _fmt(row: pd.Series) -> str:
 
584
  if chosen is None or chosen.empty:
585
  chosen_sigma = None
586
  chosen_mu = None
587
+ sugg_table = empty_suggestion_df()
588
  else:
589
  chosen_sigma = float(chosen["sigma_hist"])
590
  chosen_mu = float(chosen["mu_capm"])
591
+ sugg_table = _holdings_table_from_row(chosen, budget=gross)
592
 
593
  pos_table = pd.DataFrame(
594
  [{
 
600
  columns=["ticker", "amount_usd", "weight_exposure", "beta"]
601
  )
602
 
603
+ progress(0.95, desc="Rendering chart...")
604
  img = plot_cml(
605
  rf_ann, erp_ann, sigma_mkt,
606
  sigma_hist, mu_capm,
 
608
  sugg_sigma_hist=chosen_sigma, sugg_mu_capm=chosen_mu
609
  )
610
 
 
611
  info = "\n".join([
612
  "### Inputs",
613
  f"- Lookback years {years_lookback}",
 
629
  "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
630
  ])
631
 
632
+ progress(1.0, desc="Done.")
 
 
633
  return (
634
+ gr.update(visible=True, value=img), # plot
635
+ gr.update(visible=True, value=info), # summary
636
+ gr.update(visible=True, value=pos_table), # positions
637
+ gr.update(visible=True, value=sugg_table), # sugg_table
638
+ gr.update(visible=True, value=csv_path), # file
639
+ gr.update(visible=True, value=txt_low), # low_txt
640
+ gr.update(visible=True, value=txt_med), # med_txt
641
+ gr.update(visible=True, value=txt_high), # high_txt
642
+ gr.update(visible=True), # md_sugg
643
+ gr.update(visible=True), # btn_low
644
+ gr.update(visible=True), # btn_med
645
+ gr.update(visible=True), # btn_high
646
  )
647
 
648
  # -------------- UI --------------
649
+ with gr.Blocks(title="Efficient Portfolio Advisor", theme=gr.themes.Soft()) as demo:
650
  gr.Markdown(
651
  "## Efficient Portfolio Advisor\n"
652
+ "Search symbols, enter **dollar amounts**, set horizon. Returns use Yahoo Finance monthly data; risk-free from FRED."
 
653
  )
654
 
655
+ with gr.Row():
656
+ with gr.Column(scale=1):
657
+ # --- Vertical flow: Search -> Button -> Matches -> Add ---
658
+ q = gr.Textbox(label="Search symbol")
659
+ search_btn = gr.Button("Search")
660
+ matches = gr.Dropdown(choices=[], label="Matches", allow_custom_value=False)
661
+ add_btn = gr.Button("Add selected to portfolio")
662
+ # ----------------------------------------------------------
663
+
664
+ gr.Markdown("### Portfolio positions")
665
+ table = gr.Dataframe(
666
+ headers=["ticker", "amount_usd"],
667
+ datatype=["str", "number"],
668
+ row_count=0,
669
+ col_count=(2, "fixed")
670
+ )
671
 
672
+ horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
673
+ lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
674
 
675
+ # compute button directly under lookback slider
676
+ run_btn = gr.Button("Compute (build dataset & suggest)")
677
 
678
+ # Suggestions section (hidden until first compute)
679
+ md_sugg = gr.Markdown("### Suggestions", visible=False)
 
680
  with gr.Row():
681
+ btn_low = gr.Button("Show Low", visible=False)
682
+ btn_med = gr.Button("Show Medium", visible=False)
683
+ btn_high = gr.Button("Show High", visible=False)
684
+ low_txt = gr.Markdown(visible=False)
685
+ med_txt = gr.Markdown(visible=False)
686
+ high_txt = gr.Markdown(visible=False)
687
+
688
+ with gr.Column(scale=1):
689
+ plot = gr.Image(label="Capital Market Line (CAPM)", type="pil", visible=False)
690
+ summary = gr.Markdown(label="Inputs & Results", visible=False)
691
+ positions = gr.Dataframe(
692
+ label="Computed positions",
693
+ headers=["ticker", "amount_usd", "weight_exposure", "beta"],
694
+ datatype=["str", "number", "number", "number"],
695
+ col_count=(4, "fixed"),
696
+ value=empty_positions_df(),
697
+ interactive=False,
698
+ visible=False
699
+ )
700
+ sugg_table = gr.Dataframe(
701
+ label="Selected suggestion holdings (% / $)",
702
+ headers=["ticker", "weight_%", "amount_$"],
703
+ datatype=["str", "number", "number"],
704
+ col_count=(3, "fixed"),
705
+ value=empty_suggestion_df(),
706
+ interactive=False,
707
+ visible=False
708
+ )
709
+ dl = gr.File(label="Generated dataset CSV", value=None, visible=False)
 
 
710
 
711
  # wire search / add / locking
712
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=matches)
 
716
  # horizon updates globals silently (no UI output)
717
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
718
 
719
+ # compute + reveal UI (default to Medium band)
720
  run_btn.click(
721
  fn=compute,
722
  inputs=[lookback, table, gr.State("Medium")],
723
  outputs=[
724
  plot, summary, positions, sugg_table, dl,
725
  low_txt, med_txt, high_txt,
726
+ md_sugg, btn_low, btn_med, btn_high,
727
  ]
728
  )
729
 
730
+ # band buttons recompute picks quickly (keep everything visible)
731
  btn_low.click(
732
  fn=compute,
733
  inputs=[lookback, table, gr.State("Low")],
734
  outputs=[
735
  plot, summary, positions, sugg_table, dl,
736
  low_txt, med_txt, high_txt,
737
+ md_sugg, btn_low, btn_med, btn_high,
738
  ]
739
  )
740
  btn_med.click(
 
743
  outputs=[
744
  plot, summary, positions, sugg_table, dl,
745
  low_txt, med_txt, high_txt,
746
+ md_sugg, btn_low, btn_med, btn_high,
747
  ]
748
  )
749
  btn_high.click(
 
752
  outputs=[
753
  plot, summary, positions, sugg_table, dl,
754
  low_txt, med_txt, high_txt,
755
+ md_sugg, btn_low, btn_med, btn_high,
756
  ]
757
  )
758
 
 
761
  RF_ANN = fetch_fred_yield_annual(RF_CODE)
762
 
763
  if __name__ == "__main__":
 
764
  demo.queue().launch(server_name="0.0.0.0", server_port=7860, show_api=False)