KasperStocks / streamlit_app.py
Syntrex's picture
Upload 5 files
55ebc85 verified
#!/usr/bin/env python3
"""
Kasper Streamlit Companion App
Dashboard for:
- Welcome / architecture notes
- Projection calculator
- Target contribution calculator
- Watchlist editing against the same local JSON files the Discord bot uses
- Server config viewer
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Tuple
import pandas as pd
import streamlit as st
BOT_NAME = "Kasper"
DATA_DIR = Path("./kasper_data")
CONFIG_PATH = DATA_DIR / "guild_config.json"
WATCHLIST_PATH = DATA_DIR / "watchlists.json"
ALERT_LOG_PATH = DATA_DIR / "alert_log.json"
DATA_DIR.mkdir(parents=True, exist_ok=True)
def load_json(path: Path, default: Any) -> Any:
if not path.exists():
return default
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception:
return default
def save_json(path: Path, data: Any) -> None:
path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
def fmt_money(v: float) -> str:
return f"${v:,.2f}"
def fmt_pct(v: float) -> str:
return f"{v*100:.2f}%"
@dataclass
class Phase:
years: int
monthly_contribution: float
lump_sum_at_start: float = 0.0
label: str = ""
def simulate_portfolio(initial_investment: float, phases: List[Phase], annual_return: float):
monthly_rate = (1 + annual_return) ** (1 / 12) - 1
balance = float(initial_investment)
total_contributed = float(initial_investment)
annual_snapshots: List[Tuple[int, float]] = []
current_year = 0
for phase in phases:
balance += phase.lump_sum_at_start
total_contributed += phase.lump_sum_at_start
for month_idx in range(1, phase.years * 12 + 1):
balance += phase.monthly_contribution
total_contributed += phase.monthly_contribution
balance *= 1 + monthly_rate
if month_idx % 12 == 0:
current_year += 1
annual_snapshots.append((current_year, balance))
return {
"annual_return": annual_return,
"ending_value": balance,
"total_contributed": total_contributed,
"profit": balance - total_contributed,
"annual_snapshots": annual_snapshots,
}
def future_value_constant_monthly(initial_investment: float, monthly_contribution: float, years: int, annual_return: float) -> float:
monthly_rate = (1 + annual_return) ** (1 / 12) - 1
balance = initial_investment
for _ in range(years * 12):
balance += monthly_contribution
balance *= 1 + monthly_rate
return balance
def required_monthly_contribution(target_value: float, years: int, annual_return: float, initial_investment: float = 0.0) -> float:
low, high = 0.0, max(target_value, 1.0)
for _ in range(200):
mid = (low + high) / 2
fv = future_value_constant_monthly(initial_investment, mid, years, annual_return)
if fv >= target_value:
high = mid
else:
low = mid
return high
st.set_page_config(page_title=f"{BOT_NAME} Dashboard", layout="wide")
st.title(f"{BOT_NAME} Streamlit Dashboard")
welcome_tab, projection_tab, target_tab, watchlist_tab, config_tab = st.tabs([
"Welcome", "Projection Calculator", "Target Calculator", "Watchlists", "Server Config"
])
with welcome_tab:
st.subheader("What Kasper does")
st.write(
"Kasper is a Discord-first long-term investing companion focused on structured retirement scenario planning, "
"watchlists, and future stock-alert workflows."
)
st.markdown(
"""
**Best split of responsibilities**
- **Discord bot:** alerts, commands, role toggle, quick-use tools
- **Streamlit UI:** deeper calculators, watchlist editing, dashboards, richer tables
**Current companion app features**
- Phased portfolio projection
- Required monthly contribution calculator
- Watchlist editor backed by local JSON storage
- Config viewer for per-server bot settings
"""
)
with projection_tab:
st.subheader("Projection Calculator")
c1, c2, c3 = st.columns(3)
with c1:
initial = st.number_input("Initial investment", min_value=0.0, value=1000.0, step=100.0)
r1 = st.number_input("Return scenario 1 (%)", min_value=0.0, value=10.0, step=0.5)
with c2:
r2 = st.number_input("Return scenario 2 (%)", min_value=0.0, value=14.0, step=0.5)
with c3:
r3 = st.number_input("Return scenario 3 (%)", min_value=0.0, value=18.0, step=0.5)
st.markdown("### Phases")
p1c1, p1c2, p1c3 = st.columns(3)
with p1c1:
p1_years = st.number_input("Phase 1 years", min_value=0, value=5, step=1)
with p1c2:
p1_monthly = st.number_input("Phase 1 monthly", min_value=0.0, value=250.0, step=25.0)
with p1c3:
p1_lump = st.number_input("Phase 1 lump at start", min_value=0.0, value=0.0, step=100.0)
p2_enabled = st.checkbox("Enable phase 2", value=True)
phases = [Phase(years=int(p1_years), monthly_contribution=float(p1_monthly), lump_sum_at_start=float(p1_lump), label="Phase 1")]
if p2_enabled:
p2c1, p2c2, p2c3 = st.columns(3)
with p2c1:
p2_years = st.number_input("Phase 2 years", min_value=0, value=5, step=1)
with p2c2:
p2_monthly = st.number_input("Phase 2 monthly", min_value=0.0, value=500.0, step=25.0)
with p2c3:
p2_lump = st.number_input("Phase 2 lump at start", min_value=0.0, value=0.0, step=100.0)
phases.append(Phase(years=int(p2_years), monthly_contribution=float(p2_monthly), lump_sum_at_start=float(p2_lump), label="Phase 2"))
if st.button("Run Projection"):
rows = []
for r in [r1 / 100, r2 / 100, r3 / 100]:
result = simulate_portfolio(initial, phases, r)
rows.append({
"Annual Return": fmt_pct(r),
"Ending Value": fmt_money(result["ending_value"]),
"Contributed": fmt_money(result["total_contributed"]),
"Profit": fmt_money(result["profit"]),
"Gain vs Contributions": fmt_pct(result["profit"] / result["total_contributed"] if result["total_contributed"] else 0),
})
st.dataframe(pd.DataFrame(rows), use_container_width=True)
with target_tab:
st.subheader("Target Contribution Calculator")
tc1, tc2, tc3 = st.columns(3)
with tc1:
target = st.number_input("Target portfolio value", min_value=1.0, value=2000000.0, step=10000.0)
with tc2:
years = st.number_input("Years to invest", min_value=1, value=30, step=1)
with tc3:
initial_target = st.number_input("Initial investment", min_value=0.0, value=0.0, step=100.0)
tr1, tr2, tr3 = st.columns(3)
with tr1:
target_r1 = st.number_input("Return 1 (%)", min_value=0.0, value=10.0, step=0.5, key="target_r1")
with tr2:
target_r2 = st.number_input("Return 2 (%)", min_value=0.0, value=14.0, step=0.5, key="target_r2")
with tr3:
target_r3 = st.number_input("Return 3 (%)", min_value=0.0, value=18.0, step=0.5, key="target_r3")
if st.button("Calculate Required Monthly Contribution"):
rows = []
for r in [target_r1 / 100, target_r2 / 100, target_r3 / 100]:
monthly = required_monthly_contribution(target, int(years), r, initial_target)
rows.append({
"Annual Return": fmt_pct(r),
"Required Monthly Contribution": fmt_money(monthly),
})
st.dataframe(pd.DataFrame(rows), use_container_width=True)
with watchlist_tab:
st.subheader("Watchlists")
watchlists = load_json(WATCHLIST_PATH, {})
guild_id = st.text_input("Guild ID", value="")
if guild_id:
current = watchlists.get(guild_id, [])
st.write("Current watchlist:", ", ".join(current) if current else "Empty")
new_ticker = st.text_input("Ticker to add", value="")
c1, c2 = st.columns(2)
with c1:
if st.button("Add ticker") and new_ticker.strip():
ticker = new_ticker.upper().strip()
if ticker not in current:
current.append(ticker)
current.sort()
watchlists[guild_id] = current
save_json(WATCHLIST_PATH, watchlists)
st.success(f"Added {ticker}")
with c2:
remove_ticker = st.text_input("Ticker to remove", value="", key="remove_ticker")
if st.button("Remove ticker") and remove_ticker.strip():
ticker = remove_ticker.upper().strip()
if ticker in current:
current.remove(ticker)
watchlists[guild_id] = current
save_json(WATCHLIST_PATH, watchlists)
st.success(f"Removed {ticker}")
with config_tab:
st.subheader("Server Config")
cfg = load_json(CONFIG_PATH, {})
alert_log = load_json(ALERT_LOG_PATH, [])
st.markdown("### Guild Config JSON")
st.json(cfg)
st.markdown("### Recent Alert Log")
st.json(alert_log[-20:])