Build & Learn

Coding Hub

Hands-on projects you can build from scratch, add to your CV, and talk about in interviews. Designed specifically for buyside careers.

Why coding matters for the buyside

Every major Hedge Fund, Private Equity firm, and Asset Manager now expects candidates to have some programming ability — even in non-quant roles. Portfolio Managers want analysts who can pull data, build dashboards, and automate workflows. These 4 projects will give you real, demonstrable skills and tangible things to put on your CV.

0 Getting Started — Setup (10 minutes)

Before any project, you need Python and a few libraries installed. Follow these steps once and you're set for all 4 projects.

Step 1: Download and install Python 3.11+ from python.org

Step 2: Open your terminal (Command Prompt on Windows, Terminal on Mac) and create a project folder:

terminal
mkdir desk-ready-projects
cd desk-ready-projects
python -m venv venv

Step 3: Activate your virtual environment:

terminal
# Mac / Linux:
source venv/bin/activate

# Windows:
venv\Scripts\activate

Step 4: Install the libraries you'll need for all projects:

terminal
pip install dash pandas plotly numpy scipy

Step 5: Create a folder for each project as you go:

terminal
mkdir project-1-portfolio-dashboard
mkdir project-2-options-pricer
mkdir project-3-market-screener
mkdir project-4-trade-blotter

That's it. You're ready to build.

Project 1: Portfolio Risk Dashboard

Build an interactive web dashboard that analyses a stock portfolio — showing returns, volatility, correlations, Value-at-Risk, and Sharpe ratio. This is exactly the kind of tool used on buy-side desks every day.

★ Beginner-Friendly ⏲ 2–3 hours Python, Dash, Plotly, Pandas 🚀 High CV Impact
Video Walkthrough: Portfolio Risk Dashboard
Step 1/8

1 Create the sample data

We'll generate realistic stock price data for 8 tickers. Save this as generate_data.py in your project-1 folder and run it. It will create a portfolio_prices.csv file.

generate_data.py
import pandas as pd
import numpy as np

np.random.seed(42)
dates = pd.bdate_range("2024-01-02", "2025-12-31")
tickers = ["AAPL", "MSFT", "GOOGL", "JPM", "GS", "NVDA", "AMZN", "BLK"]
start_prices = [185, 375, 140, 170, 385, 480, 150, 790]

data = {"Date": dates}
for ticker, start in zip(tickers, start_prices):
    # Random walk with drift (realistic daily returns)
    daily_returns = np.random.normal(0.0004, 0.018, len(dates))
    prices = [start]
    for r in daily_returns[1:]:
        prices.append(round(prices[-1] * (1 + r), 2))
    data[ticker] = prices[:len(dates)]

df = pd.DataFrame(data)
df.to_csv("portfolio_prices.csv", index=False)
print(f"Created portfolio_prices.csv with {len(df)} rows and {len(tickers)} tickers")
print(df.head())
terminal
cd project-1-portfolio-dashboard
python generate_data.py

2 Build the dashboard

This is the main file. It creates a full interactive dashboard with 4 panels: cumulative returns chart, correlation heatmap, risk metrics table, and a VaR histogram. Save this as app.py.

app.py
import dash
from dash import html, dcc, callback, Input, Output
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np

# ── Load data ──
df = pd.read_csv("portfolio_prices.csv", parse_dates=["Date"])
tickers = [c for c in df.columns if c != "Date"]

# ── Calculate returns ──
returns = df.set_index("Date")[tickers].pct_change().dropna()
cum_returns = (1 + returns).cumprod() - 1

# ── Dash app ──
app = dash.Dash(__name__, title="Portfolio Risk Dashboard")

app.layout = html.Div(style={"fontFamily": "Inter, sans-serif", "padding": "24px",
    "maxWidth": "1200px", "margin": "0 auto", "backgroundColor": "#f8fafc"}, children=[

    html.H1("Portfolio Risk Dashboard",
        style={"fontSize": "24px", "fontWeight": "800", "marginBottom": "4px"}),
    html.P("Interactive risk analytics for an 8-stock portfolio",
        style={"color": "#64748b", "marginBottom": "24px"}),

    # Ticker selector
    html.Label("Select tickers:", style={"fontWeight": "600", "fontSize": "14px"}),
    dcc.Dropdown(id="ticker-select", options=[{"label": t, "value": t} for t in tickers],
        value=tickers, multi=True,
        style={"marginBottom": "24px"}),

    # Charts grid
    html.Div(style={"display": "grid", "gridTemplateColumns": "1fr 1fr",
        "gap": "20px"}, children=[

        # Cumulative Returns
        html.Div([
            html.H3("Cumulative Returns", style={"fontSize": "16px", "fontWeight": "700",
                "marginBottom": "8px"}),
            dcc.Graph(id="cum-returns-chart")
        ], style={"background": "#fff", "padding": "20px", "borderRadius": "12px",
            "border": "1px solid #e2e8f0"}),

        # Correlation Heatmap
        html.Div([
            html.H3("Return Correlations", style={"fontSize": "16px", "fontWeight": "700",
                "marginBottom": "8px"}),
            dcc.Graph(id="corr-heatmap")
        ], style={"background": "#fff", "padding": "20px", "borderRadius": "12px",
            "border": "1px solid #e2e8f0"}),

        # Risk Metrics Table
        html.Div([
            html.H3("Risk Metrics", style={"fontSize": "16px", "fontWeight": "700",
                "marginBottom": "8px"}),
            html.Div(id="risk-table")
        ], style={"background": "#fff", "padding": "20px", "borderRadius": "12px",
            "border": "1px solid #e2e8f0"}),

        # VaR Distribution
        html.Div([
            html.H3("Portfolio Return Distribution & VaR", style={"fontSize": "16px",
                "fontWeight": "700", "marginBottom": "8px"}),
            dcc.Graph(id="var-hist")
        ], style={"background": "#fff", "padding": "20px", "borderRadius": "12px",
            "border": "1px solid #e2e8f0"}),
    ])
])

@callback(
    [Output("cum-returns-chart", "figure"),
     Output("corr-heatmap", "figure"),
     Output("risk-table", "children"),
     Output("var-hist", "figure")],
    Input("ticker-select", "value"))
def update(selected):
    if not selected:
        selected = tickers

    sel_returns = returns[selected]
    sel_cum = cum_returns[selected]

    # 1. Cumulative returns line chart
    fig1 = px.line(sel_cum.reset_index(), x="Date", y=selected,
        labels={"value": "Return", "variable": "Ticker"})
    fig1.update_layout(margin=dict(l=0,r=0,t=10,b=0), height=300,
        yaxis_tickformat=".0%", legend=dict(orientation="h", y=-0.15))

    # 2. Correlation heatmap
    corr = sel_returns.corr()
    fig2 = px.imshow(corr, text_auto=".2f", color_continuous_scale="RdBu_r",
        zmin=-1, zmax=1)
    fig2.update_layout(margin=dict(l=0,r=0,t=10,b=0), height=300)

    # 3. Risk metrics table
    portfolio_returns = sel_returns.mean(axis=1)  # equal weight
    metrics = []
    for t in selected:
        ann_vol = sel_returns[t].std() * np.sqrt(252)
        ann_ret = sel_returns[t].mean() * 252
        sharpe = ann_ret / ann_vol if ann_vol > 0 else 0
        var_95 = np.percentile(sel_returns[t], 5)
        metrics.append({"Ticker": t,
            "Ann. Return": f"{ann_ret:.1%}",
            "Ann. Volatility": f"{ann_vol:.1%}",
            "Sharpe Ratio": f"{sharpe:.2f}",
            "VaR (95%)": f"{var_95:.2%}"})

    table = html.Table(style={"width": "100%", "borderCollapse": "collapse",
        "fontSize": "13px"}, children=[
        html.Thead(html.Tr([html.Th(col, style={"textAlign": "left",
            "padding": "8px", "borderBottom": "1px solid #e2e8f0",
            "color": "#64748b", "fontSize": "11px", "textTransform": "uppercase"})
            for col in metrics[0].keys()])),
        html.Tbody([html.Tr([html.Td(v, style={"padding": "6px 8px",
            "borderBottom": "1px solid #f1f5f9"})
            for v in row.values()]) for row in metrics])
    ])

    # 4. VaR histogram
    fig4 = go.Figure()
    fig4.add_trace(go.Histogram(x=portfolio_returns, nbinsx=50,
        marker_color="#0ea5e9", name="Daily Returns"))
    var_95 = np.percentile(portfolio_returns, 5)
    fig4.add_vline(x=var_95, line_dash="dash", line_color="red",
        annotation_text=f"95% VaR: {var_95:.2%}")
    fig4.update_layout(margin=dict(l=0,r=0,t=10,b=0), height=300,
        xaxis_tickformat=".1%", showlegend=False)

    return fig1, fig2, table, fig4

if __name__ == "__main__":
    app.run(debug=True, port=8050)

3 Run it

Open your terminal, navigate to the project folder, and run:

terminal
python app.py

Then open http://localhost:8050 in your browser. You should see a full interactive dashboard with 4 panels. Try selecting and deselecting tickers — everything updates in real time.

✅ CV Bullet Point

"Built an interactive portfolio risk dashboard in Python (Dash/Plotly) analysing returns, correlations, Sharpe ratios, and Value-at-Risk across an 8-stock portfolio with real-time filtering."

🚀 Extensions to make it even stronger

  • Pull live data from Yahoo Finance using the yfinance library instead of sample data
  • Add portfolio weighting sliders so users can adjust allocations
  • Add a rolling VaR chart that shows risk over time
  • Deploy it online using Render or Railway (free tier) so you can share a live link in interviews

Project 2: Options Pricer & Greeks Visualiser

Build an interactive Black-Scholes options pricing calculator with live Greeks charts. Adjust spot price, strike, volatility, interest rate, and time to expiry with sliders — and watch how option prices and Greeks change in real time. This is what derivatives desks use daily.

★★ Intermediate ⏲ 2–3 hours Python, Dash, SciPy, NumPy 🚀 Very High CV Impact
Video Walkthrough: Options Pricer & Greeks
Step 1/7

1 Black-Scholes engine

Create bs_model.py in your project-2 folder. This contains the full Black-Scholes pricing model and all 5 Greeks.

bs_model.py
import numpy as np
from scipy.stats import norm

def d1(S, K, T, r, sigma):
    return (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))

def d2(S, K, T, r, sigma):
    return d1(S, K, T, r, sigma) - sigma * np.sqrt(T)

def call_price(S, K, T, r, sigma):
    return S * norm.cdf(d1(S, K, T, r, sigma)) - K * np.exp(-r * T) * norm.cdf(d2(S, K, T, r, sigma))

def put_price(S, K, T, r, sigma):
    return K * np.exp(-r * T) * norm.cdf(-d2(S, K, T, r, sigma)) - S * norm.cdf(-d1(S, K, T, r, sigma))

def delta(S, K, T, r, sigma, option_type="call"):
    d = d1(S, K, T, r, sigma)
    return norm.cdf(d) if option_type == "call" else norm.cdf(d) - 1

def gamma(S, K, T, r, sigma):
    return norm.pdf(d1(S, K, T, r, sigma)) / (S * sigma * np.sqrt(T))

def vega(S, K, T, r, sigma):
    return S * norm.pdf(d1(S, K, T, r, sigma)) * np.sqrt(T) / 100  # per 1% vol

def theta(S, K, T, r, sigma, option_type="call"):
    d1_val = d1(S, K, T, r, sigma)
    d2_val = d2(S, K, T, r, sigma)
    term1 = -(S * norm.pdf(d1_val) * sigma) / (2 * np.sqrt(T))
    if option_type == "call":
        return (term1 - r * K * np.exp(-r * T) * norm.cdf(d2_val)) / 365
    else:
        return (term1 + r * K * np.exp(-r * T) * norm.cdf(-d2_val)) / 365

def rho(S, K, T, r, sigma, option_type="call"):
    d2_val = d2(S, K, T, r, sigma)
    if option_type == "call":
        return K * T * np.exp(-r * T) * norm.cdf(d2_val) / 100
    else:
        return -K * T * np.exp(-r * T) * norm.cdf(-d2_val) / 100

2 Build the dashboard

Create app.py. This gives you interactive sliders for every input parameter and 6 live charts showing how the option price and Greeks change as you move the spot price.

app.py
import dash
from dash import html, dcc, callback, Input, Output
import plotly.graph_objects as go
import numpy as np
from bs_model import call_price, put_price, delta, gamma, vega, theta, rho

app = dash.Dash(__name__, title="Options Pricer")

slider_style = {"marginBottom": "16px"}
label_style = {"fontWeight": "600", "fontSize": "13px", "display": "block", "marginBottom": "4px"}

app.layout = html.Div(style={"fontFamily": "Inter, sans-serif", "padding": "24px",
    "maxWidth": "1200px", "margin": "0 auto", "backgroundColor": "#f8fafc"}, children=[
    html.H1("Black-Scholes Options Pricer & Greeks",
        style={"fontSize": "24px", "fontWeight": "800", "marginBottom": "24px"}),

    html.Div(style={"display": "grid", "gridTemplateColumns": "300px 1fr", "gap": "24px"}, children=[
        # Left panel: sliders
        html.Div(style={"background": "#fff", "padding": "24px", "borderRadius": "12px",
            "border": "1px solid #e2e8f0"}, children=[
            html.H3("Parameters", style={"fontSize": "16px", "fontWeight": "700",
                "marginBottom": "16px"}),

            html.Label("Spot Price (S)", style=label_style),
            dcc.Slider(id="spot", min=50, max=200, value=100, step=1,
                marks={50: "50", 100: "100", 150: "150", 200: "200"},
                tooltip={"placement": "bottom"}),

            html.Label("Strike Price (K)", style=label_style),
            dcc.Slider(id="strike", min=50, max=200, value=100, step=1,
                marks={50: "50", 100: "100", 150: "150", 200: "200"},
                tooltip={"placement": "bottom"}),

            html.Label("Volatility (%)", style=label_style),
            dcc.Slider(id="vol", min=5, max=80, value=25, step=1,
                marks={5: "5%", 25: "25%", 50: "50%", 80: "80%"},
                tooltip={"placement": "bottom"}),

            html.Label("Risk-Free Rate (%)", style=label_style),
            dcc.Slider(id="rate", min=0, max=10, value=4, step=0.25,
                marks={0: "0%", 5: "5%", 10: "10%"},
                tooltip={"placement": "bottom"}),

            html.Label("Time to Expiry (days)", style=label_style),
            dcc.Slider(id="tte", min=1, max=365, value=90, step=1,
                marks={1: "1d", 90: "90d", 180: "180d", 365: "1Y"},
                tooltip={"placement": "bottom"}),

            html.Div(id="price-display", style={"marginTop": "20px", "padding": "16px",
                "background": "#f0f9ff", "borderRadius": "10px", "border": "1px solid #bae6fd"})
        ]),

        # Right panel: charts
        html.Div(style={"display": "grid", "gridTemplateColumns": "1fr 1fr",
            "gap": "16px"}, children=[
            html.Div([dcc.Graph(id="delta-chart")], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(id="gamma-chart")], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(id="vega-chart")], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(id="theta-chart")], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
        ])
    ])
])

@callback(
    [Output("price-display", "children"),
     Output("delta-chart", "figure"), Output("gamma-chart", "figure"),
     Output("vega-chart", "figure"), Output("theta-chart", "figure")],
    [Input("spot", "value"), Input("strike", "value"),
     Input("vol", "value"), Input("rate", "value"), Input("tte", "value")])
def update(S, K, vol_pct, rate_pct, days):
    sigma = vol_pct / 100
    r = rate_pct / 100
    T = days / 365

    cp = call_price(S, K, T, r, sigma)
    pp = put_price(S, K, T, r, sigma)

    price_display = html.Div([
        html.Div(f"Call: ${cp:.2f}", style={"fontSize": "18px", "fontWeight": "700",
            "color": "#059669"}),
        html.Div(f"Put: ${pp:.2f}", style={"fontSize": "18px", "fontWeight": "700",
            "color": "#dc2626", "marginTop": "4px"}),
    ])

    # Generate spot range for charts
    spots = np.linspace(S * 0.6, S * 1.4, 100)
    layout = dict(margin=dict(l=0,r=0,t=30,b=0), height=220,
        xaxis_title="Spot Price", template="plotly_white")

    # Delta chart
    fig_d = go.Figure()
    fig_d.add_trace(go.Scatter(x=spots, y=[delta(s, K, T, r, sigma, "call") for s in spots],
        name="Call", line=dict(color="#059669")))
    fig_d.add_trace(go.Scatter(x=spots, y=[delta(s, K, T, r, sigma, "put") for s in spots],
        name="Put", line=dict(color="#dc2626")))
    fig_d.update_layout(title="Delta", **layout)

    # Gamma chart
    fig_g = go.Figure()
    fig_g.add_trace(go.Scatter(x=spots, y=[gamma(s, K, T, r, sigma) for s in spots],
        name="Gamma", line=dict(color="#7c3aed")))
    fig_g.update_layout(title="Gamma", showlegend=False, **layout)

    # Vega chart
    fig_v = go.Figure()
    fig_v.add_trace(go.Scatter(x=spots, y=[vega(s, K, T, r, sigma) for s in spots],
        name="Vega", line=dict(color="#d97706")))
    fig_v.update_layout(title="Vega (per 1% vol)", showlegend=False, **layout)

    # Theta chart
    fig_t = go.Figure()
    fig_t.add_trace(go.Scatter(x=spots, y=[theta(s, K, T, r, sigma, "call") for s in spots],
        name="Call", line=dict(color="#059669")))
    fig_t.add_trace(go.Scatter(x=spots, y=[theta(s, K, T, r, sigma, "put") for s in spots],
        name="Put", line=dict(color="#dc2626")))
    fig_t.update_layout(title="Theta (daily)", **layout)

    return price_display, fig_d, fig_g, fig_v, fig_t

if __name__ == "__main__":
    app.run(debug=True, port=8051)

3 Run it

terminal
cd project-2-options-pricer
python app.py

Open http://localhost:8051. Move the sliders and watch call/put prices and all Greeks update instantly. Try moving volatility to see its effect on Vega. Move time to expiry to 1 day and watch Theta explode.

✅ CV Bullet Point

"Developed an interactive Black-Scholes options pricing dashboard with real-time Greeks visualisation (Delta, Gamma, Vega, Theta), built in Python using Dash and SciPy."

🚀 Extensions

  • Add implied volatility calculator — given a market price, solve for the vol using Newton's method
  • Add a P&L surface (3D plot) showing payoff across spot and vol at expiry
  • Add multi-leg strategies (spreads, straddles, butterflies) with combined payoff diagrams
  • Price exotic options (barriers, digital) using Monte Carlo simulation

Project 3: Market Screener & Sector Heatmap

Build a stock screener with filtering, sorting, and an interactive sector performance heatmap. Analysts use tools like this to scan for ideas and monitor sector rotation. You'll learn data tables, filtering logic, and heatmap visualisation.

★★ Intermediate ⏲ 2–3 hours Python, Dash, DataTable, Plotly 🚀 High CV Impact
Video Walkthrough: Market Screener & Heatmap
Step 1/6

1 Create the sample data

Generate a universe of 40 stocks across 6 sectors with fundamental data. Save as generate_data.py.

generate_data.py
import pandas as pd
import numpy as np

np.random.seed(123)
sectors = {
    "Technology": ["AAPL", "MSFT", "GOOGL", "NVDA", "META", "CRM", "ORCL"],
    "Financials": ["JPM", "GS", "MS", "BLK", "C", "BAC", "WFC"],
    "Healthcare": ["JNJ", "PFE", "UNH", "MRK", "ABBV", "LLY"],
    "Energy": ["XOM", "CVX", "COP", "SLB", "EOG", "MPC"],
    "Consumer": ["AMZN", "TSLA", "HD", "NKE", "SBUX", "MCD", "PG"],
    "Industrials": ["BA", "CAT", "GE", "HON", "UPS", "RTX", "LMT"],
}

rows = []
for sector, tickers in sectors.items():
    for ticker in tickers:
        rows.append({
            "Ticker": ticker,
            "Sector": sector,
            "Market Cap ($B)": round(np.random.uniform(50, 3000), 1),
            "P/E Ratio": round(np.random.uniform(8, 45), 1),
            "Div Yield (%)": round(np.random.uniform(0, 4.5), 2),
            "1M Return (%)": round(np.random.normal(1.5, 6), 2),
            "3M Return (%)": round(np.random.normal(4, 10), 2),
            "YTD Return (%)": round(np.random.normal(8, 18), 2),
            "Volatility (%)": round(np.random.uniform(15, 55), 1),
            "Beta": round(np.random.uniform(0.6, 1.8), 2),
        })

df = pd.DataFrame(rows)
df.to_csv("stock_universe.csv", index=False)
print(f"Created stock_universe.csv with {len(df)} stocks across {len(sectors)} sectors")

2 Build the dashboard

Two tabs: a sortable/filterable stock screener table and a sector performance treemap heatmap.

app.py
import dash
from dash import html, dcc, dash_table, callback, Input, Output
import plotly.express as px
import pandas as pd

df = pd.read_csv("stock_universe.csv")
sectors = sorted(df["Sector"].unique())

app = dash.Dash(__name__, title="Market Screener")

app.layout = html.Div(style={"fontFamily": "Inter, sans-serif", "padding": "24px",
    "maxWidth": "1200px", "margin": "0 auto", "backgroundColor": "#f8fafc"}, children=[

    html.H1("Market Screener & Sector Heatmap",
        style={"fontSize": "24px", "fontWeight": "800", "marginBottom": "24px"}),

    dcc.Tabs(id="tabs", value="screener", children=[
        dcc.Tab(label="Stock Screener", value="screener"),
        dcc.Tab(label="Sector Heatmap", value="heatmap"),
    ], style={"marginBottom": "24px"}),

    html.Div(id="tab-content")
])

@callback(Output("tab-content", "children"), Input("tabs", "value"))
def render_tab(tab):
    if tab == "screener":
        return html.Div([
            # Filters row
            html.Div(style={"display": "flex", "gap": "16px", "marginBottom": "16px",
                "flexWrap": "wrap"}, children=[
                html.Div([
                    html.Label("Sector", style={"fontWeight": "600", "fontSize": "13px"}),
                    dcc.Dropdown(id="sector-filter", options=[{"label": s, "value": s}
                        for s in sectors], multi=True, placeholder="All sectors"),
                ], style={"flex": "1", "minWidth": "200px"}),
                html.Div([
                    html.Label("Max P/E", style={"fontWeight": "600", "fontSize": "13px"}),
                    dcc.Slider(id="pe-filter", min=5, max=50, value=50, step=1,
                        marks={5: "5", 15: "15", 25: "25", 50: "50"},
                        tooltip={"placement": "bottom"}),
                ], style={"flex": "1", "minWidth": "200px"}),
                html.Div([
                    html.Label("Min YTD Return (%)", style={"fontWeight": "600",
                        "fontSize": "13px"}),
                    dcc.Slider(id="ytd-filter", min=-30, max=50, value=-30, step=1,
                        marks={-30: "-30%", 0: "0%", 25: "25%", 50: "50%"},
                        tooltip={"placement": "bottom"}),
                ], style={"flex": "1", "minWidth": "200px"}),
            ]),
            html.Div(id="screener-table")
        ])
    else:
        # Sector heatmap
        sector_agg = df.groupby("Sector").agg({
            "YTD Return (%)": "mean", "Market Cap ($B)": "sum",
            "Volatility (%)": "mean", "Ticker": "count"
        }).reset_index()
        sector_agg.columns = ["Sector", "Avg YTD Return (%)", "Total Mkt Cap ($B)",
            "Avg Vol (%)", "# Stocks"]

        fig = px.treemap(df, path=["Sector", "Ticker"], values="Market Cap ($B)",
            color="YTD Return (%)", color_continuous_scale="RdYlGn",
            color_continuous_midpoint=0,
            hover_data=["P/E Ratio", "Volatility (%)"])
        fig.update_layout(margin=dict(l=0,r=0,t=30,b=0), height=500)

        return html.Div([
            dcc.Graph(figure=fig),
            html.P("Size = Market Cap. Colour = YTD Return (green = positive, red = negative).",
                style={"fontSize": "13px", "color": "#64748b", "marginTop": "8px"})
        ])

@callback(Output("screener-table", "children"),
    [Input("sector-filter", "value"), Input("pe-filter", "value"),
     Input("ytd-filter", "value")])
def update_screener(sectors_sel, max_pe, min_ytd):
    filtered = df.copy()
    if sectors_sel:
        filtered = filtered[filtered["Sector"].isin(sectors_sel)]
    filtered = filtered[filtered["P/E Ratio"] <= max_pe]
    filtered = filtered[filtered["YTD Return (%)"] >= min_ytd]
    filtered = filtered.sort_values("YTD Return (%)", ascending=False)

    return dash_table.DataTable(
        data=filtered.to_dict("records"),
        columns=[{"name": c, "id": c} for c in filtered.columns],
        sort_action="native",
        filter_action="native",
        page_size=20,
        style_header={"backgroundColor": "#f8fafc", "fontWeight": "700",
            "fontSize": "12px", "color": "#475569", "borderBottom": "2px solid #e2e8f0"},
        style_cell={"padding": "8px 12px", "fontSize": "13px",
            "borderBottom": "1px solid #f1f5f9"},
        style_data_conditional=[
            {"if": {"column_id": "YTD Return (%)", "filter_query":
                "{YTD Return (%)} > 0"}, "color": "#059669", "fontWeight": "600"},
            {"if": {"column_id": "YTD Return (%)", "filter_query":
                "{YTD Return (%)} < 0"}, "color": "#dc2626", "fontWeight": "600"},
        ]
    )

if __name__ == "__main__":
    app.run(debug=True, port=8052)

3 Run it

terminal
cd project-3-market-screener
python generate_data.py
python app.py

Open http://localhost:8052. Filter by sector, P/E, and YTD return. Switch to the Heatmap tab to see the treemap visualisation. Click sectors to drill down into individual stocks.

✅ CV Bullet Point

"Built a stock screener and sector heatmap dashboard in Python (Dash) with dynamic filtering across 40 stocks, sortable data tables, and a treemap visualisation coloured by YTD performance."

🚀 Extensions

  • Add momentum scoring (rank by 1M + 3M + YTD returns) to create a signal
  • Add AG Grid enterprise for grouped rows by sector with aggregations
  • Connect to a free API (e.g. Alpha Vantage, Financial Modeling Prep) for live data
  • Add an export-to-CSV button so analysts can download filtered results

Project 4: Automated Trade Blotter & Risk Report

Build a trade blotter dashboard that processes a day's trades, shows P&L by strategy and sector, flags potential duplicate bookings, and generates a downloadable HTML risk report. This is directly relevant to middle-office, trading assistant, and portfolio analytics roles.

★★★ Advanced ⏲ 3–4 hours Python, Dash, Pandas, HTML reports 🚀 Very High CV Impact
Video Walkthrough: Trade Blotter & Risk Report
Step 1/6

1 Create sample trade data

Generate a realistic day's worth of trades across multiple strategies, books, and counterparties.

generate_data.py
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

np.random.seed(77)
n_trades = 60
strategies = ["Global Macro", "L/S Equity", "Stat Arb", "Event Driven", "Relative Value"]
books = ["BOOK-A", "BOOK-B", "BOOK-C"]
counterparties = ["Goldman Sachs", "JP Morgan", "Morgan Stanley", "Barclays", "Citi", "BAML"]
tickers = ["AAPL", "MSFT", "GOOGL", "NVDA", "JPM", "GS", "AMZN", "TSLA", "META",
           "XOM", "BAC", "JNJ", "PFE", "BA", "CAT", "HD", "CRM", "BLK"]
sides = ["BUY", "SELL"]

base_time = datetime(2026, 3, 26, 8, 0, 0)
trades = []
for i in range(n_trades):
    ticker = np.random.choice(tickers)
    side = np.random.choice(sides)
    qty = int(np.random.choice([100, 200, 500, 1000, 2000, 5000]))
    price = round(np.random.uniform(50, 500), 2)
    notional = qty * price
    pnl = round(np.random.normal(0, notional * 0.005), 2)

    trades.append({
        "TradeID": f"TRD-{10000 + i}",
        "Time": (base_time + timedelta(minutes=np.random.randint(0, 510))).strftime("%H:%M:%S"),
        "Ticker": ticker,
        "Side": side,
        "Quantity": qty,
        "Price": price,
        "Notional": round(notional, 2),
        "Strategy": np.random.choice(strategies),
        "Book": np.random.choice(books),
        "Counterparty": np.random.choice(counterparties),
        "PnL": pnl,
    })

# Add 2 intentional duplicates (same ticker, qty, price, close timestamps)
dup = trades[5].copy()
dup["TradeID"] = "TRD-10090"
dup["Time"] = trades[5]["Time"]  # same time = likely duplicate
trades.append(dup)
dup2 = trades[12].copy()
dup2["TradeID"] = "TRD-10091"
trades.append(dup2)

df = pd.DataFrame(trades).sort_values("Time").reset_index(drop=True)
df.to_csv("trades.csv", index=False)
print(f"Created trades.csv with {len(df)} trades ({n_trades} real + 2 duplicates)")

2 Build the dashboard

3-tab dashboard: Trade Blotter (with duplicate flags), P&L Analytics (by strategy, sector, counterparty), and a Report Generator that creates a downloadable HTML risk summary.

app.py
import dash
from dash import html, dcc, dash_table, callback, Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime

df = pd.read_csv("trades.csv")

# ── Flag duplicates: same Ticker + Qty + Price + Time
dup_cols = ["Ticker", "Quantity", "Price", "Time"]
df["IsDuplicate"] = df.duplicated(subset=dup_cols, keep=False)

app = dash.Dash(__name__, title="Trade Blotter")

app.layout = html.Div(style={"fontFamily": "Inter, sans-serif", "padding": "24px",
    "maxWidth": "1200px", "margin": "0 auto", "backgroundColor": "#f8fafc"}, children=[

    html.H1("Trade Blotter & Risk Report",
        style={"fontSize": "24px", "fontWeight": "800", "marginBottom": "4px"}),
    html.P(f"Date: {datetime.now().strftime('%d %B %Y')} | "
           f"{len(df)} trades | {df['IsDuplicate'].sum()} potential duplicates",
        style={"color": "#64748b", "marginBottom": "24px"}),

    # KPI row
    html.Div(style={"display": "flex", "gap": "16px", "marginBottom": "24px",
        "flexWrap": "wrap"}, children=[
        _kpi("Total Notional", f"${df['Notional'].sum():,.0f}"),
        _kpi("Total P&L", f"${df['PnL'].sum():,.0f}",
            "#059669" if df["PnL"].sum() >= 0 else "#dc2626"),
        _kpi("# Trades", f"{len(df)}"),
        _kpi("# Strategies", f"{df['Strategy'].nunique()}"),
        _kpi("Duplicates", f"{df['IsDuplicate'].sum()}",
            "#dc2626" if df["IsDuplicate"].sum() > 0 else "#059669"),
    ]),

    dcc.Tabs(id="tabs", value="blotter", children=[
        dcc.Tab(label="Trade Blotter", value="blotter"),
        dcc.Tab(label="P&L Analytics", value="analytics"),
        dcc.Tab(label="Generate Report", value="report"),
    ], style={"marginBottom": "24px"}),

    html.Div(id="tab-content"),
])

def _kpi(label, value, color="#0f172a"):
    return html.Div(style={"background": "#fff", "border": "1px solid #e2e8f0",
        "borderRadius": "10px", "padding": "16px 20px", "flex": "1",
        "minWidth": "140px"}, children=[
        html.Div(label, style={"fontSize": "12px", "color": "#94a3b8",
            "textTransform": "uppercase", "letterSpacing": "1px", "fontWeight": "600"}),
        html.Div(value, style={"fontSize": "20px", "fontWeight": "800", "color": color})
    ])

@callback(Output("tab-content", "children"), Input("tabs", "value"))
def render(tab):
    if tab == "blotter":
        styled = df.copy()
        return dash_table.DataTable(
            data=styled.to_dict("records"),
            columns=[{"name": c, "id": c} for c in styled.columns],
            sort_action="native", filter_action="native", page_size=25,
            style_header={"backgroundColor": "#f8fafc", "fontWeight": "700",
                "fontSize": "12px", "borderBottom": "2px solid #e2e8f0"},
            style_cell={"padding": "8px 10px", "fontSize": "13px",
                "borderBottom": "1px solid #f1f5f9"},
            style_data_conditional=[
                {"if": {"filter_query": "{IsDuplicate} = True"},
                    "backgroundColor": "#fef2f2", "color": "#dc2626"},
                {"if": {"column_id": "PnL", "filter_query": "{PnL} > 0"},
                    "color": "#059669", "fontWeight": "600"},
                {"if": {"column_id": "PnL", "filter_query": "{PnL} < 0"},
                    "color": "#dc2626", "fontWeight": "600"},
                {"if": {"column_id": "Side", "filter_query": '{Side} = "BUY"'},
                    "color": "#0369a1"},
                {"if": {"column_id": "Side", "filter_query": '{Side} = "SELL"'},
                    "color": "#7c3aed"},
            ]
        )

    elif tab == "analytics":
        # P&L by strategy
        strat_pnl = df.groupby("Strategy")["PnL"].sum().reset_index().sort_values("PnL")
        fig1 = px.bar(strat_pnl, x="PnL", y="Strategy", orientation="h",
            color="PnL", color_continuous_scale="RdYlGn", color_continuous_midpoint=0)
        fig1.update_layout(title="P&L by Strategy", margin=dict(l=0,r=0,t=40,b=0),
            height=300, showlegend=False)

        # Notional by counterparty
        cpty = df.groupby("Counterparty")["Notional"].sum().reset_index().sort_values("Notional")
        fig2 = px.bar(cpty, x="Notional", y="Counterparty", orientation="h",
            color_discrete_sequence=["#0ea5e9"])
        fig2.update_layout(title="Notional by Counterparty", margin=dict(l=0,r=0,t=40,b=0),
            height=300)

        # Trade count timeline
        df["Hour"] = pd.to_datetime(df["Time"], format="%H:%M:%S").dt.hour
        timeline = df.groupby("Hour").size().reset_index(name="Trades")
        fig3 = px.bar(timeline, x="Hour", y="Trades", color_discrete_sequence=["#7c3aed"])
        fig3.update_layout(title="Trades by Hour", margin=dict(l=0,r=0,t=40,b=0), height=250)

        # P&L by book
        book_pnl = df.groupby("Book")["PnL"].sum().reset_index()
        fig4 = px.pie(book_pnl, values="PnL", names="Book", color_discrete_sequence=
            ["#0ea5e9", "#7c3aed", "#d97706"])
        fig4.update_layout(title="P&L Split by Book", margin=dict(l=0,r=0,t=40,b=0),
            height=250)

        return html.Div(style={"display": "grid", "gridTemplateColumns": "1fr 1fr",
            "gap": "16px"}, children=[
            html.Div([dcc.Graph(figure=fig1)], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(figure=fig2)], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(figure=fig3)], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
            html.Div([dcc.Graph(figure=fig4)], style={"background": "#fff",
                "borderRadius": "12px", "border": "1px solid #e2e8f0", "padding": "12px"}),
        ])

    else:
        return html.Div(style={"background": "#fff", "borderRadius": "12px",
            "border": "1px solid #e2e8f0", "padding": "32px", "textAlign": "center"}, children=[
            html.H3("Generate End-of-Day Risk Report", style={"marginBottom": "8px"}),
            html.P("Creates an HTML file with a complete summary of today's trading.",
                style={"color": "#64748b", "marginBottom": "20px"}),
            html.Button("Generate Report", id="gen-btn",
                style={"padding": "12px 28px", "borderRadius": "10px", "border": "none",
                    "background": "#0369a1", "color": "#fff", "fontSize": "15px",
                    "fontWeight": "700", "cursor": "pointer"}),
            html.Div(id="report-status", style={"marginTop": "16px"})
        ])

@callback(Output("report-status", "children"),
    Input("gen-btn", "n_clicks"), prevent_initial_call=True)
def generate_report(n):
    strat_summary = df.groupby("Strategy").agg(
        Trades=("TradeID", "count"), Notional=("Notional", "sum"),
        PnL=("PnL", "sum")).reset_index()

    report_html = f"""<html><head><style>
    body{{font-family:Inter,sans-serif;padding:40px;max-width:800px;margin:0 auto}}
    h1{{font-size:22px}} h2{{font-size:16px;color:#0369a1;margin-top:24px}}
    table{{width:100%;border-collapse:collapse;margin:12px 0}}
    th{{background:#f8fafc;text-align:left;padding:8px;border-bottom:2px solid #e2e8f0;
    font-size:12px;text-transform:uppercase;color:#64748b}}
    td{{padding:6px 8px;border-bottom:1px solid #f1f5f9;font-size:13px}}
    .green{{color:#059669;font-weight:700}} .red{{color:#dc2626;font-weight:700}}
    </style></head><body>
    <h1>End-of-Day Risk Report</h1>
    <p>{datetime.now().strftime('%d %B %Y')} | Generated automatically</p>
    <h2>Summary</h2>
    <p>Total Trades: {len(df)} | Total Notional: ${df['Notional'].sum():,.0f} |
    P&L: <span class="{'green' if df['PnL'].sum()>=0 else 'red'}">
    ${df['PnL'].sum():,.0f}</span> |
    Duplicates Flagged: {df['IsDuplicate'].sum()}</p>
    <h2>By Strategy</h2>{strat_summary.to_html(index=False)}
    <h2>Duplicate Trades</h2>
    {df[df['IsDuplicate']].to_html(index=False) if df['IsDuplicate'].any() else '<p>None</p>'}
    </body></html>"""

    with open("eod_report.html", "w") as f:
        f.write(report_html)

    return html.Div([
        html.P("Report generated successfully!", style={"color": "#059669",
            "fontWeight": "700"}),
        html.P("Saved as eod_report.html in your project folder.",
            style={"color": "#64748b", "fontSize": "13px"})
    ])

if __name__ == "__main__":
    app.run(debug=True, port=8053)

3 Run it

terminal
cd project-4-trade-blotter
python generate_data.py
python app.py

Open http://localhost:8053. Browse the blotter (duplicate trades highlighted red), check the analytics tab for P&L breakdowns, and generate the HTML report. Open eod_report.html in your browser to see the final output.

✅ CV Bullet Point

"Built an automated trade blotter and risk reporting tool in Python (Dash) processing 60+ daily trades, flagging duplicate bookings, visualising P&L by strategy/counterparty, and generating end-of-day HTML risk reports."

🚀 Extensions

  • Add email functionality — auto-send the report to a distribution list using smtplib
  • Add a real-time mode that refreshes every 30 seconds (use dcc.Interval)
  • Add cross-book (xbook) trade detection and balance checking
  • Connect to a database (SQLite) instead of CSV for persistent storage