Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# app.py
|
| 2 |
-
import os, io, math, time, warnings
|
| 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 |
-
#
|
| 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
|
| 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)
|
| 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 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|
| 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 |
-
|
|
|
|
| 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(
|