Tulitula commited on
Commit
0dfa99d
·
verified ·
1 Parent(s): 064bf5c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +101 -79
app.py CHANGED
@@ -256,6 +256,9 @@ def build_synthetic_dataset(universe_user: List[str],
256
  erp_ann: float,
257
  sigma_mkt: float,
258
  n_rows: int = SYNTH_ROWS) -> pd.DataFrame:
 
 
 
259
  rng = np.random.default_rng(12345)
260
  assets = list(universe_user)
261
  if len(assets) == 0:
@@ -370,7 +373,7 @@ def suggest_one_per_band(synth: pd.DataFrame, sigma_mkt: float, universe_user: L
370
  out[band.lower()] = chosen
371
  return out
372
 
373
- # -------------- UI helpers (restored) --------------
374
  def empty_positions_df():
375
  return pd.DataFrame(columns=["ticker", "amount_usd", "weight_exposure", "beta"])
376
 
@@ -458,18 +461,18 @@ def compute(
458
 
459
  symbols = [t for t in df["ticker"].tolist() if t]
460
  if len(symbols) == 0:
461
- hide = gr.update(visible=False)
462
  return (
463
  None, "Add at least one ticker.", empty_positions_df(), empty_suggestion_df(), None,
464
- hide, hide, hide, hide, hide, hide, hide
 
465
  )
466
 
467
  symbols = validate_tickers(symbols, years_lookback)
468
  if len(symbols) == 0:
469
- hide = gr.update(visible=False)
470
  return (
471
  None, "Could not validate any tickers.", empty_positions_df(), empty_suggestion_df(), None,
472
- hide, hide, hide, hide, hide, hide, hide
 
473
  )
474
 
475
  global UNIVERSE
@@ -486,10 +489,10 @@ def compute(
486
  # Weights
487
  gross = sum(abs(v) for v in amounts.values())
488
  if gross <= 1e-12:
489
- hide = gr.update(visible=False)
490
  return (
491
  None, "All amounts are zero.", empty_positions_df(), empty_suggestion_df(), None,
492
- hide, hide, hide, hide, hide, hide, hide
 
493
  )
494
  weights = {k: v / gross for k, v in amounts.items()}
495
 
@@ -500,7 +503,7 @@ def compute(
500
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
501
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
502
 
503
- # Synthetic dataset & suggestions
504
  user_universe = list(symbols)
505
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
506
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
@@ -529,6 +532,7 @@ def compute(
529
  else:
530
  chosen_sigma = float(chosen["sigma_hist"])
531
  chosen_mu = float(chosen["mu_capm"])
 
532
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
533
 
534
  pos_table = pd.DataFrame(
@@ -548,6 +552,7 @@ def compute(
548
  sugg_sigma_hist=chosen_sigma, sugg_mu_capm=chosen_mu
549
  )
550
 
 
551
  info = "\n".join([
552
  "### Inputs",
553
  f"- Lookback years {years_lookback}",
@@ -564,18 +569,17 @@ def compute(
564
  f"- **Same σ as your portfolio** → Market weight **{a_sigma:.2f}**, Bills weight **{b_sigma:.2f}** → E[r] **{mu_eff_same_sigma:.2%}**",
565
  f"- **Same E[r] as your portfolio** → Market weight **{a_mu:.2f}**, Bills weight **{b_mu:.2f}** → σ **{sigma_eff_same_mu:.2%}**",
566
  "",
567
- "_How to replicate:_ use a broad market ETF (e.g., VOO) for **Market** and a T-bill/MMF for **Bills**. "
568
- "Weights may exceed 1 (leverage) or be negative (borrowing). If leverage isn’t allowed, scale toward 1.0."
 
569
  ])
 
570
 
571
- show = gr.update(visible=True)
572
  return (
573
  img, info, pos_table, sugg_table, csv_path,
574
- show, # suggestions header visible
575
- gr.update(value=txt_low, visible=True),
576
- gr.update(value=txt_med, visible=True),
577
- gr.update(value=txt_high, visible=True),
578
- gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) # buttons visible
579
  )
580
 
581
  # -------------- UI --------------
@@ -586,93 +590,111 @@ with gr.Blocks(title="Efficient Portfolio Advisor") as demo:
586
  "Plot shows **your CAPM point on the CML** plus efficient market/bills points."
587
  )
588
 
589
- # All left-stacked
590
- with gr.Column():
591
- # --- Search / Add flow (vertical) ---
592
- q = gr.Textbox(label="Search symbol")
593
- search_btn = gr.Button("Search") # directly under the search box
594
- search_note = gr.Markdown()
595
- matches = gr.Dropdown(choices=[], label="Matches")
596
- add_btn = gr.Button("Add selected to portfolio") # after matches
597
-
598
- # --- Portfolio table ---
599
- gr.Markdown("### Portfolio positions")
600
- table = gr.Dataframe(
601
- headers=["ticker", "amount_usd"],
602
- datatype=["str", "number"],
603
- row_count=0,
604
- col_count=(2, "fixed")
605
- )
606
 
607
- # --- Controls ---
608
- horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
609
- lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
610
- run_btn = gr.Button("Compute (build dataset & suggest)") # right under the slider
611
-
612
- # --- Results ---
613
- plot = gr.Image(label="Capital Market Line (CAPM)", type="pil")
614
- summary = gr.Markdown(label="Inputs & Results")
615
- positions = gr.Dataframe(
616
- label="Computed positions",
617
- headers=["ticker", "amount_usd", "weight_exposure", "beta"],
618
- datatype=["str", "number", "number", "number"],
619
- col_count=(4, "fixed"),
620
- value=empty_positions_df(),
621
- interactive=False
622
- )
623
- sugg_table = gr.Dataframe(
624
- label="Selected suggestion holdings (% / $)",
625
- headers=["ticker", "weight_%", "amount_$"],
626
- datatype=["str", "number", "number"],
627
- col_count=(3, "fixed"),
628
- value=empty_suggestion_df(),
629
- interactive=False
630
- )
631
- dl = gr.File(label="Generated dataset CSV", value=None, visible=True)
632
-
633
- # --- Suggestions (hidden until Compute) ---
634
- sugg_hdr = gr.Markdown("### Suggestions", visible=False)
635
- with gr.Row():
636
- btn_low = gr.Button("Show Low", visible=False)
637
- btn_med = gr.Button("Show Medium", visible=False)
638
- btn_high = gr.Button("Show High", visible=False)
639
- low_txt = gr.Markdown(visible=False)
640
- med_txt = gr.Markdown(visible=False)
641
- high_txt = gr.Markdown(visible=False)
642
-
643
- # --- wiring ---
 
644
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=[search_note, matches])
645
- add_btn.click(fn=add_symbol, inputs=[matches, table], outputs=[table, search_note])
646
- table.change(fn=lock_ticker_column, inputs=table, outputs=table)
647
 
 
648
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
649
 
650
- # compute (default Medium band); also reveal Suggestions UI on success
651
  run_btn.click(
652
  fn=compute,
653
  inputs=[lookback, table, gr.State("Medium")],
654
  outputs=[
655
  plot, summary, positions, sugg_table, dl,
656
- sugg_hdr, low_txt, med_txt, high_txt, # content + visibility
657
- btn_low, btn_med, btn_high # visibility only
 
658
  ]
659
  )
660
 
661
- # band buttons reuse compute; keep them visible after first compute
662
  btn_low.click(
663
  fn=compute,
664
  inputs=[lookback, table, gr.State("Low")],
665
- outputs=[plot, summary, positions, sugg_table, dl, sugg_hdr, low_txt, med_txt, high_txt, btn_low, btn_med, btn_high]
 
 
 
 
 
666
  )
667
  btn_med.click(
668
  fn=compute,
669
  inputs=[lookback, table, gr.State("Medium")],
670
- outputs=[plot, summary, positions, sugg_table, dl, sugg_hdr, low_txt, med_txt, high_txt, btn_low, btn_med, btn_high]
 
 
 
 
 
671
  )
672
  btn_high.click(
673
  fn=compute,
674
  inputs=[lookback, table, gr.State("High")],
675
- outputs=[plot, summary, positions, sugg_table, dl, sugg_hdr, low_txt, med_txt, high_txt, btn_low, btn_med, btn_high]
 
 
 
 
 
676
  )
677
 
678
  # initialize risk-free at launch
 
256
  erp_ann: float,
257
  sigma_mkt: float,
258
  n_rows: int = SYNTH_ROWS) -> pd.DataFrame:
259
+ """
260
+ Generate long-only mixes **from exactly the user's tickers** (VOO included only if the user holds it).
261
+ """
262
  rng = np.random.default_rng(12345)
263
  assets = list(universe_user)
264
  if len(assets) == 0:
 
373
  out[band.lower()] = chosen
374
  return out
375
 
376
+ # -------------- UI helpers --------------
377
  def empty_positions_df():
378
  return pd.DataFrame(columns=["ticker", "amount_usd", "weight_exposure", "beta"])
379
 
 
461
 
462
  symbols = [t for t in df["ticker"].tolist() if t]
463
  if len(symbols) == 0:
 
464
  return (
465
  None, "Add at least one ticker.", empty_positions_df(), empty_suggestion_df(), None,
466
+ "", "", "",
467
+ None, None, None, None, None, None, None, None, None
468
  )
469
 
470
  symbols = validate_tickers(symbols, years_lookback)
471
  if len(symbols) == 0:
 
472
  return (
473
  None, "Could not validate any tickers.", empty_positions_df(), empty_suggestion_df(), None,
474
+ "", "", "",
475
+ None, None, None, None, None, None, None, None, None
476
  )
477
 
478
  global UNIVERSE
 
489
  # Weights
490
  gross = sum(abs(v) for v in amounts.values())
491
  if gross <= 1e-12:
 
492
  return (
493
  None, "All amounts are zero.", empty_positions_df(), empty_suggestion_df(), None,
494
+ "", "", "",
495
+ rf_ann, erp_ann, sigma_mkt, None, None, None, None, None, None
496
  )
497
  weights = {k: v / gross for k, v in amounts.items()}
498
 
 
503
  a_sigma, b_sigma, mu_eff_same_sigma = efficient_same_sigma(sigma_hist, rf_ann, erp_ann, sigma_mkt)
504
  a_mu, b_mu, sigma_eff_same_mu = efficient_same_return(mu_capm, rf_ann, erp_ann, sigma_mkt)
505
 
506
+ # Synthetic dataset & suggestions — exactly the user's tickers
507
  user_universe = list(symbols)
508
  synth = build_synthetic_dataset(user_universe, covA, betas, rf_ann, erp_ann, sigma_mkt, n_rows=SYNTH_ROWS)
509
  csv_path = os.path.join(DATA_DIR, f"investor_profiles_{int(time.time())}.csv")
 
532
  else:
533
  chosen_sigma = float(chosen["sigma_hist"])
534
  chosen_mu = float(chosen["mu_capm"])
535
+ # holdings table from chosen suggestion
536
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
537
 
538
  pos_table = pd.DataFrame(
 
552
  sugg_sigma_hist=chosen_sigma, sugg_mu_capm=chosen_mu
553
  )
554
 
555
+ # ---------- summary text ----------
556
  info = "\n".join([
557
  "### Inputs",
558
  f"- Lookback years {years_lookback}",
 
569
  f"- **Same σ as your portfolio** → Market weight **{a_sigma:.2f}**, Bills weight **{b_sigma:.2f}** → E[r] **{mu_eff_same_sigma:.2%}**",
570
  f"- **Same E[r] as your portfolio** → Market weight **{a_mu:.2f}**, Bills weight **{b_mu:.2f}** → σ **{sigma_eff_same_mu:.2%}**",
571
  "",
572
+ "_How to replicate:_ use a broad market ETF (e.g., VOO) for the **Market** leg and a T-bill/money-market fund for **Bills**. ",
573
+ "Weights can be >1 or negative (e.g., Market > 1 and Bills < 0 implies leverage/borrowing). ",
574
+ "If leverage isn’t allowed, scale both weights proportionally toward 1.0 to fit your constraints.",
575
  ])
576
+ # -----------------------------------
577
 
 
578
  return (
579
  img, info, pos_table, sugg_table, csv_path,
580
+ txt_low, txt_med, txt_high,
581
+ rf_ann, erp_ann, sigma_mkt, sigma_hist, mu_capm, mu_eff_same_sigma, sigma_eff_same_mu,
582
+ chosen_sigma, chosen_mu
 
 
583
  )
584
 
585
  # -------------- UI --------------
 
590
  "Plot shows **your CAPM point on the CML** plus efficient market/bills points."
591
  )
592
 
593
+ with gr.Row():
594
+ with gr.Column(scale=1):
595
+ # --- Vertical flow: Search -> Button -> Matches -> Add ---
596
+ q = gr.Textbox(label="Search symbol")
597
+ search_btn = gr.Button("Search")
598
+ search_note = gr.Markdown()
599
+ matches = gr.Dropdown(choices=[], label="Matches")
600
+ add_btn = gr.Button("Add selected to portfolio")
601
+ # ----------------------------------------------------------
602
+
603
+ gr.Markdown("### Portfolio positions")
604
+ table = gr.Dataframe(
605
+ headers=["ticker", "amount_usd"],
606
+ datatype=["str", "number"],
607
+ row_count=0,
608
+ col_count=(2, "fixed")
609
+ )
610
 
611
+ horizon = gr.Number(label="Horizon in years (1–100)", value=HORIZON_YEARS, precision=0)
612
+ lookback = gr.Slider(1, 15, value=DEFAULT_LOOKBACK_YEARS, step=1, label="Lookback years for betas & covariances")
613
+
614
+ # --- Compute button directly under lookback slider ---
615
+ run_btn = gr.Button("Compute (build dataset & suggest)")
616
+ # -----------------------------------------------------
617
+
618
+ gr.Markdown("### Suggestions")
619
+ with gr.Row():
620
+ btn_low = gr.Button("Show Low")
621
+ btn_med = gr.Button("Show Medium")
622
+ btn_high = gr.Button("Show High")
623
+ low_txt = gr.Markdown()
624
+ med_txt = gr.Markdown()
625
+ high_txt = gr.Markdown()
626
+
627
+ with gr.Column(scale=1):
628
+ plot = gr.Image(label="Capital Market Line (CAPM)", type="pil")
629
+ summary = gr.Markdown(label="Inputs & Results")
630
+ positions = gr.Dataframe(
631
+ label="Computed positions",
632
+ headers=["ticker", "amount_usd", "weight_exposure", "beta"],
633
+ datatype=["str", "number", "number", "number"],
634
+ col_count=(4, "fixed"),
635
+ value=empty_positions_df(),
636
+ interactive=False
637
+ )
638
+ sugg_table = gr.Dataframe(
639
+ label="Selected suggestion holdings (% / $)",
640
+ headers=["ticker", "weight_%", "amount_$"],
641
+ datatype=["str", "number", "number"],
642
+ col_count=(3, "fixed"),
643
+ value=empty_suggestion_df(),
644
+ interactive=False
645
+ )
646
+ dl = gr.File(label="Generated dataset CSV", value=None, visible=True)
647
+
648
+ # wire search / add / locking
649
  search_btn.click(fn=search_tickers_cb, inputs=q, outputs=[search_note, matches])
650
+ add_btn.click(fn=add_symbol, inputs=[matches, table], outputs=[table, search_note])
651
+ table.change(fn=lock_ticker_column, inputs=table, outputs=table)
652
 
653
+ # horizon updates globals silently (no UI output)
654
  horizon.change(fn=set_horizon, inputs=horizon, outputs=[])
655
 
656
+ # compute + render (default to Medium band)
657
  run_btn.click(
658
  fn=compute,
659
  inputs=[lookback, table, gr.State("Medium")],
660
  outputs=[
661
  plot, summary, positions, sugg_table, dl,
662
+ low_txt, med_txt, high_txt,
663
+ gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(),
664
+ gr.State(), gr.State()
665
  ]
666
  )
667
 
668
+ # band buttons recompute picks quickly
669
  btn_low.click(
670
  fn=compute,
671
  inputs=[lookback, table, gr.State("Low")],
672
+ outputs=[
673
+ plot, summary, positions, sugg_table, dl,
674
+ low_txt, med_txt, high_txt,
675
+ gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(),
676
+ gr.State(), gr.State()
677
+ ]
678
  )
679
  btn_med.click(
680
  fn=compute,
681
  inputs=[lookback, table, gr.State("Medium")],
682
+ outputs=[
683
+ plot, summary, positions, sugg_table, dl,
684
+ low_txt, med_txt, high_txt,
685
+ gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(),
686
+ gr.State(), gr.State()
687
+ ]
688
  )
689
  btn_high.click(
690
  fn=compute,
691
  inputs=[lookback, table, gr.State("High")],
692
+ outputs=[
693
+ plot, summary, positions, sugg_table, dl,
694
+ low_txt, med_txt, high_txt,
695
+ gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(), gr.State(),
696
+ gr.State(), gr.State()
697
+ ]
698
  )
699
 
700
  # initialize risk-free at launch