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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +108 -132
app.py CHANGED
@@ -392,20 +392,14 @@ def set_horizon(years: float):
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
 
@@ -478,44 +472,18 @@ def compute(
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,40 +493,32 @@ def compute(
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,7 +527,8 @@ def compute(
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:
@@ -588,6 +549,7 @@ def compute(
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(
@@ -600,7 +562,6 @@ def compute(
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,
@@ -629,37 +590,40 @@ def compute(
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(
@@ -672,30 +636,30 @@ with gr.Blocks(title="Efficient Portfolio Advisor", theme=gr.themes.Soft()) as d
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 (% / $)",
@@ -703,57 +667,69 @@ with gr.Blocks(title="Efficient Portfolio Advisor", theme=gr.themes.Soft()) as d
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)
713
  add_btn.click(fn=add_symbol_table_only, inputs=[matches, table], outputs=table)
714
  table.change(fn=lock_ticker_column, inputs=table, outputs=table)
715
-
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(
741
  fn=compute,
742
- inputs=[lookback, table, gr.State("Medium")],
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(
750
  fn=compute,
751
- inputs=[lookback, table, gr.State("High")],
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
 
759
  # initialize risk-free at launch
@@ -761,4 +737,4 @@ RF_CODE = fred_series_for_horizon(HORIZON_YEARS)
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)
 
392
  def search_tickers_cb(q: str):
393
  opts = yahoo_search(q)
394
  if not opts:
395
+ opts = ["No matches found"]
396
+ # First match auto-selected, helper info inside dropdown
397
+ first = opts[0] if opts and ("No matches" not in opts[0]) else None
398
+ info = "Select a symbol and click 'Add selected to portfolio'." if first else "No matches."
399
+ return gr.update(choices=opts, value=first, info=info)
 
 
 
 
 
 
400
 
401
  def add_symbol(selection: str, table: Optional[pd.DataFrame]):
402
+ if (not selection) or ("No matches" in selection) or ("Select a symbol" in selection) or ("type above" in selection):
403
  return table if isinstance(table, pd.DataFrame) else pd.DataFrame(columns=["ticker","amount_usd"]), "Pick a valid match first."
404
  symbol = selection.split("|")[0].strip().upper()
405
 
 
472
 
473
  symbols = [t for t in df["ticker"].tolist() if t]
474
  if len(symbols) == 0:
 
 
 
 
475
  return (
476
+ None, "Add at least one ticker.", empty_positions_df(), empty_suggestion_df(), None,
477
+ "", "", "",
478
+ None, None, None, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
479
  )
480
 
481
  symbols = validate_tickers(symbols, years_lookback)
482
  if len(symbols) == 0:
 
 
 
 
483
  return (
484
+ None, "Could not validate any tickers.", empty_positions_df(), empty_suggestion_df(), None,
485
+ "", "", "",
486
+ None, None, None, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
487
  )
488
 
489
  global UNIVERSE
 
493
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
494
  rf_ann = RF_ANN
495
 
496
+ progress(0.25, desc="Downloading prices & computing moments...")
497
+
498
+ # Moments
499
  moms = estimate_all_moments_aligned(symbols, years_lookback, rf_ann)
500
  betas, covA, erp_ann, sigma_mkt = moms["betas"], moms["cov_ann"], moms["erp_ann"], moms["sigma_m_ann"]
501
 
502
+ # Weights
503
  gross = sum(abs(v) for v in amounts.values())
504
  if gross <= 1e-12:
 
 
 
 
505
  return (
506
+ None, "All amounts are zero.", empty_positions_df(), empty_suggestion_df(), None,
507
+ "", "", "",
508
+ rf_ann, erp_ann, sigma_mkt, None, None, None, None, None, None
 
 
 
 
 
 
 
 
 
509
  )
510
  weights = {k: v / gross for k, v in amounts.items()}
511
 
512
+ # Portfolio CAPM stats
513
  beta_p, mu_capm, sigma_hist = portfolio_stats(weights, covA, betas, rf_ann, erp_ann)
514
 
515
+ progress(0.55, desc="Building synthetic dataset...")
516
+
517
+ # Efficient alternatives on CML
518
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
519
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
520
 
521
+ # Synthetic dataset & suggestions — exactly the user's tickers
522
  user_universe = list(symbols)
523
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
524
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
 
527
  except Exception:
528
  csv_path = None
529
 
530
+ progress(0.8, desc="Selecting suggestions...")
531
+
532
  picks = suggest_one_per_band(synth, sigma_mkt, user_universe)
533
 
534
  def _fmt(row: pd.Series) -> str:
 
549
  else:
550
  chosen_sigma = float(chosen["sigma_hist"])
551
  chosen_mu = float(chosen["mu_capm"])
552
+ # holdings table from chosen suggestion
553
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
554
 
555
  pos_table = pd.DataFrame(
 
562
  columns=["ticker", "amount_usd", "weight_exposure", "beta"]
563
  )
564
 
 
565
  img = plot_cml(
566
  rf_ann, erp_ann, sigma_mkt,
567
  sigma_hist, mu_capm,
 
590
  "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
591
  ])
592
 
593
+ progress(1.0, desc="Done")
594
+
595
  return (
596
+ img, info, pos_table, sugg_table, csv_path,
597
+ txt_low, txt_med, txt_high,
598
+ rf_ann, erp_ann, sigma_mkt, sigma_hist, mu_capm, mu_eff_same_sigma, sigma_eff_same_mu,
599
+ chosen_sigma, chosen_mu
 
 
 
 
 
 
 
 
600
  )
601
 
602
  # -------------- UI --------------
603
+ APP_CSS = """
604
+ :root {
605
+ --accent: hsl(340 70% 50%);
606
+ --radius: 14px;
607
+ }
608
+ * { font-family: ui-sans-serif, system-ui, -apple-system, "Inter", Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
609
+ .gr-button { border-radius: var(--radius); }
610
+ .gr-textbox, .gr-dropdown, .gr-slider, .gr-number, .gr-dataframe { border-radius: var(--radius); }
611
+ """
612
+
613
+ with gr.Blocks(title="Efficient Portfolio Advisor", css=APP_CSS) as demo:
614
+ gr.Markdown("## Efficient Portfolio Advisor")
615
+
616
+ # States to absorb extra returns
617
+ s1 = gr.State(); s2 = gr.State(); s3 = gr.State(); s4 = gr.State(); s5 = gr.State()
618
+ s6 = gr.State(); s7 = gr.State(); s8 = gr.State(); s9 = gr.State()
619
 
620
  with gr.Row():
621
+ with gr.Column(scale=1) as left_col:
622
+ # --- Input column (full-width before compute) ---
623
  q = gr.Textbox(label="Search symbol")
624
  search_btn = gr.Button("Search")
625
+ matches = gr.Dropdown(choices=[], label="Matches", value=None, info="")
626
  add_btn = gr.Button("Add selected to portfolio")
 
627
 
628
  gr.Markdown("### Portfolio positions")
629
  table = gr.Dataframe(
 
636
  horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
637
  lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
638
 
639
+ # Compute button (shows progress bar while compute runs)
640
  run_btn = gr.Button("Compute (build dataset & suggest)")
641
 
642
+ # Suggestions (hidden until first compute)
643
+ gr.Markdown("### Suggestions", visible=False, elem_id="sugg_hdr")
644
+ with gr.Row(visible=False) as sugg_btn_row:
645
+ btn_low = gr.Button("Show Low")
646
+ btn_med = gr.Button("Show Medium")
647
+ btn_high = gr.Button("Show High")
648
+ low_txt = gr.Markdown(visible=False)
649
+ med_txt = gr.Markdown(visible=False)
650
  high_txt = gr.Markdown(visible=False)
651
 
652
+ # --- Output column (hidden until first compute) ---
653
+ with gr.Column(scale=1, visible=False) as right_col:
654
+ plot = gr.Image(label="Capital Market Line (CAPM)", type="pil")
655
+ summary = gr.Markdown(label="Inputs & Results")
656
  positions = gr.Dataframe(
657
  label="Computed positions",
658
  headers=["ticker", "amount_usd", "weight_exposure", "beta"],
659
  datatype=["str", "number", "number", "number"],
660
  col_count=(4, "fixed"),
661
  value=empty_positions_df(),
662
+ interactive=False
 
663
  )
664
  sugg_table = gr.Dataframe(
665
  label="Selected suggestion holdings (% / $)",
 
667
  datatype=["str", "number", "number"],
668
  col_count=(3, "fixed"),
669
  value=empty_suggestion_df(),
670
+ interactive=False
 
671
  )
672
+ dl = gr.File(label="Generated dataset CSV", value=None, visible=True)
673
 
674
+ # --- Wiring ---
675
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=matches)
676
  add_btn.click(fn=add_symbol_table_only, inputs=[matches, table], outputs=table)
677
  table.change(fn=lock_ticker_column, inputs=table, outputs=table)
 
 
678
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
679
 
680
+ # Compute (default band = Medium) -> populate outputs
681
+ run_compute = run_btn.click(
682
  fn=compute,
683
  inputs=[lookback, table, gr.State("Medium")],
684
  outputs=[
685
  plot, summary, positions, sugg_table, dl,
686
  low_txt, med_txt, high_txt,
687
+ s1, s2, s3, s4, s5, s6, s7, s8, s9
688
+ ],
689
+ # show a global progress bar during compute
690
+ show_progress=True,
691
+ scroll_to_output=True,
692
+ )
693
+
694
+ # After compute: reveal right column + suggestions
695
+ def _reveal():
696
+ return (
697
+ gr.update(visible=True), # right_col
698
+ gr.update(visible=True), # sugg header
699
+ gr.update(visible=True), # sugg_btn_row
700
+ gr.update(visible=True), # low_txt
701
+ gr.update(visible=True), # med_txt
702
+ gr.update(visible=True), # high_txt
703
+ )
704
+
705
+ run_compute.then(
706
+ fn=_reveal,
707
+ inputs=[],
708
+ outputs=[right_col, demo.get_component("sugg_hdr"), sugg_btn_row, low_txt, med_txt, high_txt]
709
  )
710
 
711
+ # Band buttons -> recompute quickly, keeping layout visible
712
+ def _band_low(): return "Low"
713
+ def _band_med(): return "Medium"
714
+ def _band_high(): return "High"
715
+
716
  btn_low.click(
717
  fn=compute,
718
+ inputs=[lookback, table, gr.State(_band_low())],
719
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
720
+ show_progress=True
 
 
 
721
  )
722
  btn_med.click(
723
  fn=compute,
724
+ inputs=[lookback, table, gr.State(_band_med())],
725
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
726
+ show_progress=True
 
 
 
727
  )
728
  btn_high.click(
729
  fn=compute,
730
+ inputs=[lookback, table, gr.State(_band_high())],
731
+ outputs=[plot, summary, positions, sugg_table, dl, low_txt, med_txt, high_txt, s1, s2, s3, s4, s5, s6, s7, s8, s9],
732
+ show_progress=True
 
 
 
733
  )
734
 
735
  # initialize risk-free at launch
 
737
  RF_ANN = fetch_fred_yield_annual(RF_CODE)
738
 
739
  if __name__ == "__main__":
740
+ demo.launch(server_name="0.0.0.0", server_port=7860, show_api=False)