VOOZH about

URL: https://dev.to/soytuber/06-portfolio-defense-dashboard-one-screen-to-rule-your-morning-3ak

⇱ [06] Portfolio Defense Dashboard — One Screen to Rule Your Morning - DEV Community


[06] Portfolio Defense Dashboard — One Screen to Rule Your Morning

This is Part 6 (final) of a 6-part series: Building Investment Systems with Python


The Morning Check

Every day, one question: "Am I safe?"

Not "what's the market doing?" Not "should I buy or sell?" Just: is my system intact?

This final article wires together everything we've built — the ALM database, the stress tester, the commitment line logic, the dividend snowball, and the Monte Carlo engine — into a single Streamlit dashboard.

One command. One screen. Every morning.

$ streamlit run dashboard.py

The Dashboard

# dashboard.py
import streamlit as st
import sqlite3
import pandas as pd
import plotly.graph_objects as go
from datetime import date, timedelta
from alm_schema import DB_PATH

st.set_page_config(page_title="Life ALM Dashboard", layout="wide")

# ─── Dark Theme ────────────────────────────────────────────
st.markdown("""
<style>
 .stApp { background-color: #0a0a0a; }
 .metric-card {
 background: #111;
 border: 1px solid #1a1a1a;
 border-radius: 4px;
 padding: 16px;
 text-align: center;
 }
 .metric-value {
 font-size: 28px;
 font-weight: 700;
 font-family: 'JetBrains Mono', monospace;
 }
 .metric-label {
 font-size: 12px;
 color: #888;
 text-transform: uppercase;
 letter-spacing: 2px;
 }
 .status-green { color: #00ff88; }
 .status-yellow { color: #ffaa00; }
 .status-red { color: #ff4444; }
</style>
""", unsafe_allow_html=True)

# ─── Data Layer ────────────────────────────────────────────
@st.cache_data(ttl=300)
def load_data():
 conn = sqlite3.connect(DB_PATH)

 # Portfolio
 holdings = pd.read_sql("""
 SELECT h.ticker, h.name, h.shares, h.cost_basis,
 p.close_price, h.shares * p.close_price as market_value
 FROM holdings h
 JOIN prices p ON h.ticker = p.ticker
 WHERE p.price_date = (SELECT MAX(price_date) FROM prices)
 """, conn)

 # Loans
 loans = pd.read_sql("SELECT * FROM loans", conn)

 # Expenses & Income
 expenses = pd.read_sql("SELECT * FROM expenses", conn)
 income = pd.read_sql("SELECT * FROM income", conn)

 # Snapshots (history)
 snapshots = pd.read_sql("SELECT * FROM snapshots ORDER BY snapshot_date", conn)

 conn.close()
 return holdings, loans, expenses, income, snapshots

holdings, loans, expenses, income, snapshots = load_data()

# ─── Calculations ──────────────────────────────────────────
portfolio_value = holdings['market_value'].sum()
total_cost = holdings['cost_basis'].sum()
unrealized_pnl = portfolio_value - total_cost

margin_loan = loans[loans['collateral_type'] == 'portfolio']
margin_balance = margin_loan['balance'].sum() if not margin_loan.empty else 0
margin_ratio = margin_balance / portfolio_value if portfolio_value > 0 else 0

credit_line = loans[loans['collateral_type'].isna()]
credit_available = credit_line['balance'].sum() if not credit_line.empty else 0

monthly_expenses = expenses['monthly_amount'].sum()
monthly_income = income['monthly_amount'].sum()
monthly_cashflow = monthly_income - monthly_expenses

annual_expenses = monthly_expenses * 12
estimated_dividends = 5_000_000 # from dividend model
fire_ratio = estimated_dividends / annual_expenses

# Status
if margin_ratio < 0.50:
 status_color = "status-green"
 status_text = "🟢 SAFE"
elif margin_ratio < 0.60:
 status_color = "status-yellow"
 status_text = "🟡 CAUTION"
else:
 status_color = "status-red"
 status_text = "🔴 DANGER"

# ─── Header ────────────────────────────────────────────────
st.markdown(f"""
# Life ALM Dashboard
**{date.today().isoformat()}** · <span class="{status_color}">{status_text}</span>
""", unsafe_allow_html=True)

# ─── Top Metrics ───────────────────────────────────────────
col1, col2, col3, col4, col5 = st.columns(5)

with col1:
 st.metric("Portfolio", f"¥{portfolio_value:,.0f}", f"¥{unrealized_pnl:+,.0f}")
with col2:
 st.metric("Margin Ratio", f"{margin_ratio:.1%}",
 delta=None, delta_color="inverse")
with col3:
 st.metric("Monthly Cashflow", f"¥{monthly_cashflow:+,.0f}")
with col4:
 st.metric("FIRE Ratio", f"{fire_ratio:.0%}")
with col5:
 # Max survivable drawdown
 total_defense = credit_available + 4_500_000 # cash
 for drop in range(1, 100):
 collateral = portfolio_value * (1 - drop/100)
 if collateral <= 0:
 max_drop = drop - 1
 break
 repay = max(0, margin_balance - collateral * 0.85)
 if repay > total_defense:
 max_drop = drop - 1
 break
 else:
 max_drop = 99
 st.metric("Max Survival", f"-{max_drop}%")

# ─── Holdings Table ────────────────────────────────────────
st.markdown("### Holdings")
display_df = holdings[['ticker', 'name', 'shares', 'close_price', 'market_value', 'cost_basis']].copy()
display_df['pnl'] = display_df['market_value'] - display_df['cost_basis']
display_df['weight'] = display_df['market_value'] / portfolio_value * 100
display_df.columns = ['Ticker', 'Name', 'Shares', 'Price', 'Value', 'Cost', 'P&L', 'Weight%']
st.dataframe(display_df.style.format({
 'Price': '¥{:,.0f}',
 'Value': '¥{:,.0f}',
 'Cost': '¥{:,.0f}',
 'P&L': '¥{:+,.0f}',
 'Weight%': '{:.1f}%',
 'Shares': '{:,}',
}), use_container_width=True, hide_index=True)

# ─── Stress Test ───────────────────────────────────────────
st.markdown("### Stress Test")

stress_data = []
for drop in range(5, 75, 5):
 collateral = portfolio_value * (1 - drop/100)
 ratio = margin_balance / collateral if collateral > 0 else float('inf')
 repay_85 = max(0, margin_balance - collateral * 0.85)
 survive = "" if repay_85 <= total_defense else ""

 if ratio > 0.85:
 zone = "🔴 Forced Liq"
 elif ratio > 0.70:
 zone = "🟠 Margin Call"
 elif ratio > 0.60:
 zone = "🟡 Frozen"
 else:
 zone = "🟢 Safe"

 stress_data.append({
 'Drop': f"-{drop}%",
 'Collateral': collateral,
 'Ratio': ratio,
 'Zone': zone,
 'Repay Needed': repay_85,
 'Survive?': survive,
 })

stress_df = pd.DataFrame(stress_data)
st.dataframe(stress_df.style.format({
 'Collateral': '¥{:,.0f}',
 'Ratio': '{:.1%}',
 'Repay Needed': '¥{:,.0f}',
}), use_container_width=True, hide_index=True)

# ─── Margin Ratio Chart ───────────────────────────────────
drops = list(range(0, 75))
ratios = [margin_balance / (portfolio_value * (1 - d/100)) * 100
 for d in drops if portfolio_value * (1 - d/100) > 0]

fig = go.Figure()
fig.add_trace(go.Scatter(x=drops[:len(ratios)], y=ratios,
 mode='lines', line=dict(color='#00ff88', width=3), name='Margin Ratio'))

# Threshold lines
fig.add_hline(y=60, line_dash="dash", line_color="#ffaa00", annotation_text="Freeze (60%)")
fig.add_hline(y=70, line_dash="dash", line_color="#ff6600", annotation_text="Call (70%)")
fig.add_hline(y=85, line_dash="dash", line_color="#ff0000", annotation_text="Forced Liq (85%)")

fig.update_layout(
 template='plotly_dark',
 paper_bgcolor='#0a0a0a',
 plot_bgcolor='#0a0a0a',
 title='Margin Ratio vs Drawdown',
 xaxis_title='Portfolio Drawdown (%)',
 yaxis_title='Margin Ratio (%)',
 height=400,
)
st.plotly_chart(fig, use_container_width=True)

# ─── Dividend Projection ──────────────────────────────────
st.markdown("### Dividend Snowball Projection")

years = list(range(11))
div_income = []
cumulative = 0
extra = 0
base_div = 5_000_000
growth = 0.06

for y in years:
 organic = base_div * (1 + growth) ** y
 total = organic + extra
 div_income.append(total)
 reinvest = total + 200_000 * 12 # additional monthly investment
 cumulative += reinvest
 extra = cumulative * 0.045

fig2 = go.Figure()
fig2.add_trace(go.Bar(
 x=[2026 + y for y in years],
 y=[d / 1_000_000 for d in div_income],
 marker_color='#00ff88',
 name='Annual Dividends'
))
fig2.add_hline(y=annual_expenses / 1_000_000, line_dash="dash",
 line_color="#ff4444", annotation_text="Annual Expenses")

fig2.update_layout(
 template='plotly_dark',
 paper_bgcolor='#0a0a0a',
 plot_bgcolor='#0a0a0a',
 title='Projected Dividend Income (¥M)',
 yaxis_title='¥ Millions',
 height=400,
)
st.plotly_chart(fig2, use_container_width=True)

# ─── Balance Sheet Summary ─────────────────────────────────
st.markdown("### Balance Sheet")
col_a, col_l = st.columns(2)

with col_a:
 st.markdown("**Assets**")
 st.write(f"Portfolio: ¥{portfolio_value:,.0f}")
 st.write(f"Cash: ¥4,500,000")
 st.write(f"Real Estate: (paid off)")
 st.write(f"**Total: ¥{portfolio_value + 4_500_000:,.0f}**")

with col_l:
 st.markdown("**Liabilities**")
 st.write(f"Margin Loan: ¥{margin_balance:,.0f}")
 st.write(f"Credit Line: ¥{credit_available:,.0f} (standby)")
 st.write(f"Monthly Burn: ¥{monthly_expenses:,.0f}/mo")
 st.write(f"**Net Worth: ¥{portfolio_value + 4_500_000 - margin_balance:,.0f}**")

# ─── Footer ────────────────────────────────────────────────
st.markdown("---")
st.markdown(
 "*Life ALM Dashboard · 枯れた技術の水平思考 · "
 "[Series: Building Investment Systems with Python]"
 "(https://dev.to/soytuber/why-im-engineering-my-fire-with-python-a-manifesto-32l7)*"
)

Deploying It

This runs on your local machine. No cloud. No subscription. No third-party access to your financial data.

# Install dependencies
pip install streamlit plotly pandas

# Run
streamlit run dashboard.py

# Or add to your startup
echo "streamlit run ~/alm/dashboard.py --server.port 8501" >> ~/.bashrc

For always-on access, add it to systemd:

# /etc/systemd/system/alm-dashboard.service
[Unit]
Description=Life ALM Dashboard

[Service]
ExecStart=/usr/bin/streamlit run /home/user/alm/dashboard.py --server.port 8501
WorkingDirectory=/home/user/alm
Restart=always

[Install]
WantedBy=multi-user.target

Open http://localhost:8501 every morning. That's your CFO briefing.


What the Dashboard Tells You

At a glance:

Metric Meaning
Portfolio Current total market value
Margin Ratio How leveraged you are (lower = safer)
Monthly Cashflow Income minus expenses
FIRE Ratio Passive income ÷ expenses (>100% = FI)
Max Survival Biggest crash you can survive

The stress test table shows exactly what happens at each drawdown level. The chart visualizes where the danger zones are. The dividend projection shows when the snowball crosses your expense line.


The Series in Retrospect

Over 6 weeks, we built:

Episode Module Purpose
01 ALM Schema The data foundation
02 Stress Tester Know your breaking point
03 Commitment Line Orthogonal defense design
04 Dividend Snowball Passive income trajectory
05 Monte Carlo FIRE as a probability
06 Dashboard Daily situational awareness

Total dependencies: sqlite3, pandas, matplotlib, numpy, streamlit, plotly. All battle-tested. All will work in 2036.


Closing Thought

The system we built answers one question: "Am I safe?"

Not "am I rich?" Not "am I beating the index?" Just: can I sustain my life, through any plausible scenario, without being forced to do anything I don't want to do?

If the answer is yes — and the Monte Carlo says it with 90%+ confidence — then you're financially independent. The cage door is open. You don't need permission to walk through it.

The rest is just life.


Every company has a CFO. Now you are your own.


Full series:

Building Investment Systems with Python — Engineering financial independence with code.