Tulitula commited on
Commit
ca24e8f
·
verified ·
1 Parent(s): 4ed264b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -15
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # app.py
2
- import os, io, math, time, warnings, json
3
  warnings.filterwarnings("ignore")
4
 
5
  from typing import List, Tuple, Dict, Optional
@@ -163,7 +163,7 @@ def estimate_all_moments_aligned(symbols: List[str], years: int, rf_ann: float):
163
  betas[s] = cov_sm / var_m
164
  betas[MARKET_TICKER] = 1.0
165
 
166
- # >>> FIX: include MARKET_TICKER in covariance so σ for portfolios holding VOO is correct
167
  asset_cols_all = list(R.columns) # includes market
168
  cov_m_all = np.cov(R[asset_cols_all].values.T, ddof=1) if asset_cols_all else np.zeros((0, 0))
169
  covA = pd.DataFrame(cov_m_all * 12.0, index=asset_cols_all, columns=asset_cols_all)
@@ -222,7 +222,7 @@ def plot_cml(rf_ann, erp_ann, sigma_mkt,
222
  plt.scatter([_pct(0)], [_pct(rf_ann)], label="Risk-free")
223
  plt.scatter([_pct(sigma_mkt)], [_pct(rf_ann + erp_ann)], label="Market")
224
 
225
- # Your CAPM point (y is clamped under CML at your σ for display)
226
  y_cml_at_sigma_p = rf_ann + slope * max(0.0, float(sigma_hist_p))
227
  y_you = min(float(mu_capm_p), y_cml_at_sigma_p)
228
  plt.scatter([_pct(sigma_hist_p)], [_pct(y_you)], label="Your CAPM point")
@@ -260,7 +260,7 @@ def build_synthetic_dataset(universe_user: List[str],
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) # <-- allow VOO if present
264
  if len(assets) == 0:
265
  return pd.DataFrame(columns=["tickers", "weights", "beta", "mu_capm", "sigma_hist"])
266
 
@@ -328,7 +328,6 @@ def rerank_and_pick_one(df_band: pd.DataFrame,
328
  embs_ok = False
329
  q = None
330
 
331
- # exposure similarity
332
  def _cos(a, b):
333
  an = np.linalg.norm(a) + 1e-12
334
  bn = np.linalg.norm(b) + 1e-12
@@ -360,11 +359,25 @@ def suggest_one_per_band(synth: pd.DataFrame, sigma_mkt: float, universe_user: L
360
  out: Dict[str, pd.Series] = {}
361
  for band in ["Low", "Medium", "High"]:
362
  lo, hi = _band_bounds(sigma_mkt, band)
363
- pick_pool = synth[(synth["sigma_hist"] >= lo) & (synth["sigma_hist"] <= hi)].copy()
364
- if pick_pool.empty:
365
- pick_pool = synth.copy()
366
- pick_pool = pick_pool.sort_values("mu_capm", ascending=False).head(50).reset_index(drop=True)
367
- chosen = rerank_and_pick_one(pick_pool, universe_user, band)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  out[band.lower()] = chosen
369
  return out
370
 
@@ -458,15 +471,15 @@ def compute(
458
  symbols = [t for t in df["ticker"].tolist() if t]
459
  if len(symbols) == 0:
460
  return None, "Add at least one ticker.", "Universe empty.", empty_positions_df(), empty_suggestion_df(), None, \
461
- "", "", "", None, None, None, None, None, None, None
462
 
463
  symbols = validate_tickers(symbols, years_lookback)
464
  if len(symbols) == 0:
465
  return None, "Could not validate any tickers.", "Universe invalid.", empty_positions_df(), empty_suggestion_df(), None, \
466
- "", "", "", None, None, None, None, None, None, None
467
 
468
  global UNIVERSE
469
- UNIVERSE = list(sorted(set(symbols)))[:MAX_TICKERS] # keep market if present
470
 
471
  df = df[df["ticker"].isin(symbols)].copy()
472
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
@@ -480,7 +493,7 @@ def compute(
480
  gross = sum(abs(v) for v in amounts.values())
481
  if gross <= 1e-12:
482
  return None, "All amounts are zero.", "Universe ok.", empty_positions_df(), empty_suggestion_df(), None, \
483
- "", "", "", None, None, None, None, None, None, None
484
  weights = {k: v / gross for k, v in amounts.items()}
485
 
486
  # Portfolio CAPM stats
@@ -504,7 +517,8 @@ def compute(
504
  def _fmt(row: pd.Series) -> str:
505
  if row is None or row.empty:
506
  return "No pick available."
507
- return f"CAPM E[r] {row['mu_capm']*100:.2f}%, σ(h) {row['sigma_hist']*100:.2f}%"
 
508
 
509
  txt_low = _fmt(picks.get("low", pd.Series(dtype=object)))
510
  txt_med = _fmt(picks.get("medium", pd.Series(dtype=object)))
@@ -519,6 +533,7 @@ def compute(
519
  else:
520
  chosen_sigma = float(chosen["sigma_hist"])
521
  chosen_mu = float(chosen["mu_capm"])
 
522
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
523
 
524
  pos_table = pd.DataFrame(
 
1
  # app.py
2
+ import os, io, math, time, warnings
3
  warnings.filterwarnings("ignore")
4
 
5
  from typing import List, Tuple, Dict, Optional
 
163
  betas[s] = cov_sm / var_m
164
  betas[MARKET_TICKER] = 1.0
165
 
166
+ # include market in covariance so σ for portfolios holding VOO is correct
167
  asset_cols_all = list(R.columns) # includes market
168
  cov_m_all = np.cov(R[asset_cols_all].values.T, ddof=1) if asset_cols_all else np.zeros((0, 0))
169
  covA = pd.DataFrame(cov_m_all * 12.0, index=asset_cols_all, columns=asset_cols_all)
 
222
  plt.scatter([_pct(0)], [_pct(rf_ann)], label="Risk-free")
223
  plt.scatter([_pct(sigma_mkt)], [_pct(rf_ann + erp_ann)], label="Market")
224
 
225
+ # Your CAPM point (y clamped to CML at your σ for display)
226
  y_cml_at_sigma_p = rf_ann + slope * max(0.0, float(sigma_hist_p))
227
  y_you = min(float(mu_capm_p), y_cml_at_sigma_p)
228
  plt.scatter([_pct(sigma_hist_p)], [_pct(y_you)], label="Your CAPM point")
 
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:
265
  return pd.DataFrame(columns=["tickers", "weights", "beta", "mu_capm", "sigma_hist"])
266
 
 
328
  embs_ok = False
329
  q = None
330
 
 
331
  def _cos(a, b):
332
  an = np.linalg.norm(a) + 1e-12
333
  bn = np.linalg.norm(b) + 1e-12
 
359
  out: Dict[str, pd.Series] = {}
360
  for band in ["Low", "Medium", "High"]:
361
  lo, hi = _band_bounds(sigma_mkt, band)
362
+ pool = synth[(synth["sigma_hist"] >= lo) & (synth["sigma_hist"] <= hi)].copy()
363
+ note = ""
364
+ if pool.empty:
365
+ # Closest-available logic to prevent "Low" showing a high-vol pick
366
+ if band.lower() == "low":
367
+ pool = synth.nsmallest(50, "sigma_hist").copy()
368
+ note = " (closest available: min σ)"
369
+ elif band.lower() == "high":
370
+ pool = synth.nlargest(50, "sigma_hist").copy()
371
+ note = " (closest available: max σ)"
372
+ else: # medium
373
+ tmp = synth.copy()
374
+ tmp["dist_med"] = (tmp["sigma_hist"] - sigma_mkt).abs()
375
+ pool = tmp.nsmallest(100, "dist_med").drop(columns=["dist_med"])
376
+ note = " (closest to market σ)"
377
+ chosen = rerank_and_pick_one(pool, universe_user, band)
378
+ if not chosen.empty:
379
+ chosen = chosen.copy()
380
+ chosen["__note__"] = note
381
  out[band.lower()] = chosen
382
  return out
383
 
 
471
  symbols = [t for t in df["ticker"].tolist() if t]
472
  if len(symbols) == 0:
473
  return None, "Add at least one ticker.", "Universe empty.", empty_positions_df(), empty_suggestion_df(), None, \
474
+ "", "", "", None, None, None, None, None, None, None, None, None
475
 
476
  symbols = validate_tickers(symbols, years_lookback)
477
  if len(symbols) == 0:
478
  return None, "Could not validate any tickers.", "Universe invalid.", empty_positions_df(), empty_suggestion_df(), None, \
479
+ "", "", "", None, None, None, None, None, None, None, None, None
480
 
481
  global UNIVERSE
482
+ UNIVERSE = list(sorted(set(symbols)))[:MAX_TICKERS]
483
 
484
  df = df[df["ticker"].isin(symbols)].copy()
485
  amounts = {r["ticker"]: float(r["amount_usd"]) for _, r in df.iterrows()}
 
493
  gross = sum(abs(v) for v in amounts.values())
494
  if gross <= 1e-12:
495
  return None, "All amounts are zero.", "Universe ok.", empty_positions_df(), empty_suggestion_df(), None, \
496
+ "", "", "", None, None, None, None, None, None, None, None, None
497
  weights = {k: v / gross for k, v in amounts.items()}
498
 
499
  # Portfolio CAPM stats
 
517
  def _fmt(row: pd.Series) -> str:
518
  if row is None or row.empty:
519
  return "No pick available."
520
+ note = str(row.get("__note__", "") or "")
521
+ return f"CAPM E[r] {row['mu_capm']*100:.2f}%, σ(h) {row['sigma_hist']*100:.2f}%{note}"
522
 
523
  txt_low = _fmt(picks.get("low", pd.Series(dtype=object)))
524
  txt_med = _fmt(picks.get("medium", pd.Series(dtype=object)))
 
533
  else:
534
  chosen_sigma = float(chosen["sigma_hist"])
535
  chosen_mu = float(chosen["mu_capm"])
536
+ # holdings table from chosen suggestion
537
  sugg_table = _holdings_table_from_row(chosen, budget=gross)
538
 
539
  pos_table = pd.DataFrame(