html`<style>
.portfolio-builder {
max-width: 1200px;
margin: 0 auto;
}
.common-params {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 30px;
}
.asset-card {
background: white;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.asset-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #e9ecef;
cursor: pointer;
user-select: none;
}
.asset-title {
font-size: 18px;
font-weight: 600;
color: #495057;
}
.asset-title::before {
content: '▼ ';
display: inline-block;
transition: transform 0.2s;
}
.asset-title.collapsed::before {
transform: rotate(-90deg);
}
.delete-btn {
background: #dc3545;
color: white;
border: none;
padding: 5px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.delete-btn:hover {
background: #c82333;
}
.asset-content {
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.asset-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #2196F3;
padding: 15px;
margin: 15px 0;
font-size: 14px;
}
.plot-legend {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
font-size: 13px;
}
.swatch {
display: inline-block;
width: 20px;
height: 12px;
margin-right: 5px;
}
.swatch-line {
display: inline-block;
width: 25px;
height: 0;
border-top: 2px solid;
margin-right: 5px;
vertical-align: middle;
}
.badge {
background: #6c757d;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
}
.portfolio-section {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 8px;
padding: 20px;
margin-top: 30px;
}
.portfolio-title {
font-size: 20px;
font-weight: 700;
color: #856404;
margin-bottom: 20px;
}
</style>`Portfolio builder
Portfolio Builder
Build your own derivatives portfolio by combining different positions in the underlying asset, futures contracts, and options. Adjust the common parameters and add multiple assets to see how they interact.
Key Concepts
- Payoff (blue line): The value of the position at maturity based on the underlying asset price (\(S_T\)), not accounting for initial costs. For a long underlying position, this represents the market value (\(S_T\) × quantity). For a short underlying position, this represents the negative of the market value (-(\(S_T\) × quantity)), reflecting the liability.
- Profit/Loss (red line): The actual profit or loss, accounting for the initial cost or premium paid/received
- Option Style: All options are assumed to be European-style, meaning they can only be exercised at expiration.
- Future Price (F₀): Automatically calculated using the cost-of-carry model: \(F_0 = S_0 e^{(r-q)T}\).
- Dividend Yield (q): The dividend yield is accounted for in the pricing of options and futures, and is also included in the profit/loss calculation for direct positions in the underlying asset.
- Transaction Costs: In real-world scenarios, transaction costs (e.g., commissions, bid-ask spreads) would reduce actual profits and increase losses.
Common Parameters
These parameters apply to all assets in your portfolio:
viewof S0_common = Inputs.range([50, 200], {step: 1, label: "Spot Price (S₀)", value: 100})
viewof T_common = Inputs.range([0.1, 2], {step: 0.1, label: "Time to Expiration (T, years)", value: 1})
viewof sigma_common = Inputs.range([0.05, 0.80], {step: 0.05, label: "Volatility (σ)", value: 0.20})
viewof r_common = Inputs.range([0, 0.15], {step: 0.01, label: "Risk-Free Rate (r, continuously compounded)", value: 0.04})
viewof q_common = Inputs.range([0, 0.10], {step: 0.01, label: "Dividend Yield (q, continuously compounded)", value: 0.00})erf = x => {
const sign = Math.sign(x);
const ax = Math.abs(x);
const t = 1 / (1 + 0.3275911 * ax);
const y = 1 - (((((1.061405429*t - 1.453152027)*t) + 1.421413741)*t - 0.284496736)*t + 0.254829592)*t*Math.exp(-ax*ax);
return sign * y;
}
// Standard normal CDF
N = z => 0.5 * (1 + erf(z / Math.SQRT2));
// Black-Scholes pricing function
blackScholes = (S, K, T, r, sigma, q) => {
if (T <= 0) return {call: Math.max(0, S - K), put: Math.max(0, K - S)};
const d1 = (Math.log(S / K) + (r - q + 0.5 * sigma * sigma) * T) / (sigma * Math.sqrt(T));
const d2 = d1 - sigma * Math.sqrt(T);
const call = S * Math.exp(-q * T) * N(d1) - K * Math.exp(-r * T) * N(d2);
const put = K * Math.exp(-r * T) * N(-d2) - S * Math.exp(-q * T) * N(-d1);
return {call, put};
}
// Futures pricing using cost-of-carry model
futuresPrice = (S, T, r, q) => {
return S * Math.exp((r - q) * T);
}mutable assets = []
mutable nextAssetId = 0
mutable collapsedAssets = new Set()
function addAsset() {
const id = nextAssetId;
mutable nextAssetId = nextAssetId + 1;
mutable assets = [...assets, {
id: id,
type: 'underlying',
position: 'long',
quantity: 1,
strike: 100
}];
}
function removeAsset(id) {
mutable assets = assets.filter(a => a.id !== id);
mutable collapsedAssets.delete(id);
}
function updateAssetType(id, value) {
mutable assets = assets.map(a => {
if (a.id !== id) return a;
// When switching to option, set appropriate position
if (value === 'option' && (a.position === 'long' || a.position === 'short')) {
return {...a, type: value, position: a.position === 'long' ? 'long-call' : 'short-call'};
}
// When switching from option to underlying/future
if (a.type === 'option' && value !== 'option') {
const isLong = a.position.includes('long');
return {...a, type: value, position: isLong ? 'long' : 'short'};
}
return {...a, type: value};
});
}
function updateAssetPosition(id, value) {
mutable assets = assets.map(a => a.id === id ? {...a, position: value} : a);
}
function updateAssetQuantity(id, value) {
mutable assets = assets.map(a => a.id === id ? {...a, quantity: value} : a);
}
function updateAssetStrike(id, value) {
mutable assets = assets.map(a => a.id === id ? {...a, strike: value} : a);
}
function toggleCollapse(id) {
if (collapsedAssets.has(id)) {
mutable collapsedAssets.delete(id);
} else {
mutable collapsedAssets.add(id);
}
mutable collapsedAssets = new Set(collapsedAssets);
}Build Your Portfolio
viewof addAssetTrigger = {
const button = html`<button style="background: #2f71d5; color: white; border: none; border-radius: 10px; padding: 0.55rem 1.3rem; font-weight: 600; cursor: pointer;">Add Asset</button>`
button.onclick = () => {
addAsset();
button.value = Date.now();
button.dispatchEvent(new CustomEvent("input"));
}
button.value = 0;
return button;
}// Calculate payoff and P&L for each asset
assetCalculations = assets.map(asset => {
const stRange = d3.range(0, 251, 2);
const F0 = futuresPrice(S0_common, T_common, r_common, q_common);
// Calculate initial cost
let initialCost = 0;
if (asset.type === 'underlying') {
initialCost = S0_common;
} else if (asset.type === 'option') {
const prices = blackScholes(S0_common, asset.strike, T_common, r_common, sigma_common, q_common);
initialCost = asset.position === 'long-call' || asset.position === 'short-call'
? prices.call
: prices.put;
} else if (asset.type === 'future') {
initialCost = 0; // Futures have no initial cost (ignoring margin)
}
// Calculate payoff and P&L for each ST value
const data = stRange.map(st => {
let payoff = 0;
if (asset.type === 'underlying') {
if (asset.position === 'long') {
payoff = st * asset.quantity;
} else {
payoff = -st * asset.quantity;
}
} else if (asset.type === 'future') {
if (asset.position === 'long') {
payoff = (st - F0) * asset.quantity;
} else {
payoff = (F0 - st) * asset.quantity;
}
} else if (asset.type === 'option') {
let optionPayoff = 0;
if (asset.position === 'long-call') {
optionPayoff = Math.max(0, st - asset.strike);
} else if (asset.position === 'short-call') {
optionPayoff = -Math.max(0, st - asset.strike);
} else if (asset.position === 'long-put') {
optionPayoff = Math.max(0, asset.strike - st);
} else if (asset.position === 'short-put') {
optionPayoff = -Math.max(0, asset.strike - st);
}
payoff = optionPayoff * asset.quantity;
}
// Calculate profit/loss
let costAdjustment = 0;
let dividendAdjustment = 0; // Initialize dividend adjustment
if (asset.type === 'underlying') {
const accumulatedDividendsPerShare = S0_common * (Math.exp(q_common * T_common) - 1); // Calculate accumulated dividends
if (asset.position === 'long') {
costAdjustment = -initialCost * asset.quantity;
dividendAdjustment = accumulatedDividendsPerShare * asset.quantity; // Add dividends for long
} else { // short
costAdjustment = initialCost * asset.quantity;
dividendAdjustment = -accumulatedDividendsPerShare * asset.quantity; // Subtract dividends for short
}
} else if (asset.type === 'option') {
if (asset.position === 'long-call' || asset.position === 'long-put') {
costAdjustment = -initialCost * asset.quantity;
} else {
costAdjustment = initialCost * asset.quantity;
}
}
const profit = payoff + costAdjustment + dividendAdjustment;
return { st, payoff, profit };
});
// Calculate individual breakeven point(s)
let breakeven = null;
if (asset.type === 'underlying') {
breakeven = S0_common;
} else if (asset.type === 'future') {
breakeven = F0;
} else if (asset.type === 'option') {
const premium = initialCost; // initialCost is the premium for options
if (asset.position === 'long-call' || asset.position === 'short-call') {
breakeven = asset.strike + premium;
} else if (asset.position === 'long-put' || asset.position === 'short-put') {
breakeven = asset.strike - premium;
}
}
return {
id: asset.id,
asset: asset,
data: data,
initialCost: initialCost,
F0: F0,
breakeven: breakeven // Add this line
};
});// Portfolio aggregation
portfolioData = {
if (assets.length === 0) return [];
const stRange = d3.range(0, 251, 2);
return stRange.map((st, i) => {
let totalPayoff = 0;
let totalProfit = 0;
assetCalculations.forEach(calc => {
totalPayoff += calc.data[i].payoff;
totalProfit += calc.data[i].profit;
});
return { st, payoff: totalPayoff, profit: totalProfit };
});
}// Render individual assets
{
if (assets.length === 0) {
return html`<div class="info-box">No assets in portfolio. Click "Add Asset" to get started.</div>`;
}
return html`<div>${assets.map((asset, index) => {
const calc = assetCalculations.find(c => c.id === asset.id);
if (!calc) return null;
// Create asset type label
let assetLabel = '';
if (asset.type === 'underlying') {
assetLabel = `${asset.position === 'long' ? 'Long' : 'Short'} Underlying`;
} else if (asset.type === 'future') {
assetLabel = `${asset.position === 'long' ? 'Long' : 'Short'} Future`;
} else if (asset.type === 'option') {
const optionType = asset.position.includes('call') ? 'Call' : 'Put';
const posType = asset.position.includes('long') ? 'Long' : 'Short';
assetLabel = `${posType} ${optionType} (K=${asset.strike})`;
}
const isCollapsed = collapsedAssets.has(asset.id);
const assetTypeInput = Inputs.select(['underlying', 'future', 'option'], {
label: 'Asset Type',
value: asset.type
});
assetTypeInput.addEventListener('input', () => updateAssetType(asset.id, assetTypeInput.value));
const positionOptions = asset.type === 'option'
? ['long-call', 'short-call', 'long-put', 'short-put']
: ['long', 'short'];
const positionInput = Inputs.select(positionOptions, {
label: 'Position',
value: asset.position
});
positionInput.addEventListener('input', () => updateAssetPosition(asset.id, positionInput.value));
const quantityInput = Inputs.range([1, 10], {
label: 'Quantity',
value: asset.quantity,
step: 1
});
quantityInput.addEventListener('input', (e) => updateAssetQuantity(asset.id, +e.target.value));
const strikeInput = asset.type === 'option' ? Inputs.range([50, 200], {
label: 'Strike Price (K)',
value: asset.strike,
step: 5
}) : null;
if (strikeInput) {
strikeInput.addEventListener('input', (e) => updateAssetStrike(asset.id, +e.target.value));
}
const card = html`
<div class="asset-card">
<div class="asset-header">
<div class="asset-title ${isCollapsed ? 'collapsed' : ''}">
Asset #${index + 1}: ${assetLabel} × ${asset.quantity}
</div>
<button class="delete-btn">Delete</button>
</div>
<div class="asset-content" style="display: ${isCollapsed ? 'none' : 'block'}">
<div class="asset-controls">
<div>${assetTypeInput}</div>
<div>${positionInput}</div>
<div>${quantityInput}</div>
${asset.type === 'option' ? html`<div>${strikeInput}</div>` : ''}
</div>
<div class="info-box">
<strong>Premium Paid/Received:</strong> $ ${(calc.initialCost * asset.quantity).toFixed(4)}
${asset.type === 'option' && asset.position.includes('short')
? ' (received as credit, subject to margin requirements)'
: asset.type === 'option' && asset.position.includes('long')
? ' (paid as debit)'
: asset.type === 'underlying' && asset.position === 'long'
? ' (paid as debit)'
: asset.type === 'underlying' && asset.position === 'short'
? ' (received as credit)'
: ''}
${calc.breakeven !== null ? html`<br><strong>Breakeven Point (S_T):</strong> ${calc.breakeven.toFixed(2)}${asset.type === 'underlying' ? ' (i.e., the initial spot price)' : ''}` : ''}
${asset.type === 'future' ? html`<br><strong>Futures Price (F₀):</strong> ${calc.F0.toFixed(4)}<br><em>(Note: Futures require margin accounts and are marked-to-market daily, implying capital commitment and liquidity risk.)</em>` : ''}
${asset.type === 'underlying' ? html`<br><strong>Spot Price (S₀):</strong> ${S0_common.toFixed(2)}` : ''}
</div>
${Plot.plot({
height: 300,
x: { label: 'S_T (Underlying Price at Maturity)', domain: [0, 250] },
y: { label: 'Value' },
grid: true,
pointer: "plot",
marks: [
Plot.ruleY([0], { stroke: 'black' }),
asset.type === 'option' ? Plot.ruleX([asset.strike], { stroke: 'gray', strokeDasharray: '4,4' }) : null,
Plot.line(calc.data, { x: 'st', y: 'payoff', stroke: '#2196F3', strokeWidth: 2 }),
Plot.line(calc.data, { x: 'st', y: 'profit', stroke: '#E91E63', strokeWidth: 2 }),
Plot.tip(calc.data, Plot.pointerX({x: "st", y: "profit", title: (d) => `S_T: ${d.st}
Payoff: ${d.payoff.toFixed(2)}
Profit: ${d.profit.toFixed(2)}`})),
Plot.crosshair(calc.data, {x: "st", y: "profit", stroke: "#444", opacity: 0.6})
].filter(m => m !== null)
})}
<div class="plot-legend">
<span><span class="swatch" style="background: #2196F3"></span> Payoff</span>
<span><span class="swatch" style="background: #E91E63"></span> Profit/Loss</span>
${asset.type === 'option'
? html`<span><span class="swatch-line" style="border-top-color: gray; border-top-style: dashed;"></span>Strike K <span class="badge">${asset.strike}</span></span>`
: ''
}
</div>
</div>
</div>
`;
// Attach event handlers
const header = card.querySelector('.asset-header');
const deleteBtn = card.querySelector('.delete-btn');
header.onclick = () => toggleCollapse(asset.id);
deleteBtn.onclick = (e) => {
e.stopPropagation();
removeAsset(asset.id);
};
return card;
})}</div>`;
}Portfolio Summary
// Portfolio summary
{
if (assets.length === 0) {
return html`<div class="info-box">Add assets to see portfolio summary.</div>`;
}
const totalInitialCost = assetCalculations.reduce((sum, calc) => {
let cost = calc.initialCost * calc.asset.quantity;
if (calc.asset.type === 'future') {
cost = 0; // Futures have no initial cash flow
} else if (calc.asset.position.startsWith('long')) {
cost = -cost; // Long positions are debits (cash outflow)
}
// Short positions (underlying, options) are credits (cash inflow), so 'cost' remains positive
return sum + cost;
}, 0);
const breakevenPoints = [];
if (portfolioData.length === 0) return breakevenPoints;
// Find points where profit crosses zero
for (let i = 0; i < portfolioData.length - 1; i++) {
const current = portfolioData[i];
const next = portfolioData[i + 1];
// Check for sign change or if profit is exactly zero
if (current.profit === 0) {
breakevenPoints.push(current.st);
} else if (current.profit < 0 && next.profit > 0) {
// Interpolate for a more precise breakeven point
const interpolatedSt = current.st + (next.st - current.st) * (0 - current.profit) / (next.profit - current.profit);
breakevenPoints.push(interpolatedSt);
} else if (current.profit > 0 && next.profit < 0) {
// Interpolate for a more precise breakeven point
const interpolatedSt = current.st + (next.st - current.st) * (0 - current.profit) / (next.profit - current.profit);
breakevenPoints.push(interpolatedSt);
}
}
// Check the last point if it's exactly zero
if (portfolioData.length > 0 && portfolioData[portfolioData.length - 1].profit === 0) {
breakevenPoints.push(portfolioData[portfolioData.length - 1].st);
}
// Remove duplicates and sort
const portfolioBreakevenPoints = [...new Set(breakevenPoints)].sort((a, b) => a - b);
return html`
<div class="portfolio-section">
<div class="portfolio-title">Combined Portfolio Payoff & Profit/Loss</div>
${Plot.plot({
height: 400,
x: { label: 'S_T (Underlying Price at Maturity)', domain: [0, 250] },
y: { label: 'Portfolio Value' },
grid: true,
pointer: "plot",
marks: [
Plot.ruleY([0], { stroke: 'black', strokeWidth: 1.5 }),
Plot.line(portfolioData, { x: 'st', y: 'payoff', stroke: '#2196F3', strokeWidth: 3 }),
Plot.line(portfolioData, { x: 'st', y: 'profit', stroke: '#E91E63', strokeWidth: 3 }),
Plot.tip(portfolioData, Plot.pointerX({x: "st", y: "profit", title: (d) => `S_T: ${d.st}
Total Payoff: ${d.payoff.toFixed(2)}
Total Profit: ${d.profit.toFixed(2)}`})),
Plot.crosshair(portfolioData, {x: "st", y: "profit", stroke: "#444", opacity: 0.6})
]
})}
<div class="plot-legend">
<span><span class="swatch" style="background: #2196F3"></span> Total Payoff</span>
<span><span class="swatch" style="background: #E91E63"></span> Total Profit/Loss</span>
</div>
<div class="info-box">
<strong>Portfolio Composition:</strong><br>
${assets.map((a, i) => {
const calc = assetCalculations.find(c => c.id === a.id);
let label = '';
if (a.type === 'underlying') {
label = `${a.position === 'long' ? 'Long' : 'Short'} ${a.quantity} × Underlying`;
} else if (a.type === 'future') {
label = `${a.position === 'long' ? 'Long' : 'Short'} ${a.quantity} × Future`;
} else {
const optionType = a.position.includes('call') ? 'Call' : 'Put';
const posType = a.position.includes('long') ? 'Long' : 'Short';
label = `${posType} ${a.quantity} × ${optionType} (K=${a.strike})`;
}
return html` • ${label}<br>`;
})}
<br>
<strong>Total Assets:</strong> ${assets.length}<br>
<strong>Net Initial Cash Flow:</strong> ${totalInitialCost < 0 ? '-' : '+'}$ ${Math.abs(totalInitialCost).toFixed(4)}
${totalInitialCost < 0 ? ' (net debit)' : ' (net credit)'}
${portfolioBreakevenPoints.length > 0
? html`<br><strong>Portfolio Breakeven Point(s) (S_T):</strong> ${portfolioBreakevenPoints.map(bp => bp.toFixed(2)).join(', ')}`
: html`<br><strong>Portfolio Breakeven Point(s) (S_T):</strong> None within the displayed range.`
}
</div>
</div>
`;
}