Hands-on projects you can build from scratch, add to your CV, and talk about in interviews. Designed specifically for buyside careers.
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.
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:
terminalmkdir 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:
terminalpip install dash pandas plotly numpy scipy
Step 5: Create a folder for each project as you go:
terminalmkdir 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.
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.
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.
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
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.
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)
Open your terminal, navigate to the project folder, and run:
terminalpython 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.
"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."
yfinance library instead of sample dataBuild 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.
Create bs_model.py in your project-2 folder. This contains the full Black-Scholes pricing model and all 5 Greeks.
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
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.
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)
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.
"Developed an interactive Black-Scholes options pricing dashboard with real-time Greeks visualisation (Delta, Gamma, Vega, Theta), built in Python using Dash and SciPy."
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.
Generate a universe of 40 stocks across 6 sectors with fundamental data. Save as 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")
Two tabs: a sortable/filterable stock screener table and a sector performance treemap heatmap.
app.pyimport 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)
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.
"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."
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.
Generate a realistic day's worth of trades across multiple strategies, books, and counterparties.
generate_data.pyimport 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)")
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.pyimport 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)
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.
"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."
smtplibdcc.Interval)