viewof usCorpRates = Inputs.form({
usd: Inputs.range([2, 8], { value: 5.0, step: 0.1, label: "USD rate (%)" }),
eur: Inputs.range([2, 8], { value: 7.6, step: 0.1, label: "EUR rate (%)" })
})Currency swaps
A currency swap allows two parties to exchange borrowing obligations denominated in different currencies. A key feature is that the two parties exchange the principal at the start of the swap and return it at the end. Companies with different credit profiles often face varying borrowing costs across currency markets. This opportunity for mutual gain exists when the companies have a comparative advantage in different credit markets, meaning the difference in their borrowing rates is not the same across currencies. Such advantages can arise from many factors, including credit quality, market access, and even differing corporate tax treatments across jurisdictions. The swap allows them to collectively exploit this inefficiency.
This tool demonstrates two distinct swap structures:
Standard Swap (Intermediary Bears FX Risk): A fully-hedged structure where the financial intermediary absorbs all foreign exchange risk. Both corporations receive a guaranteed, single-currency borrowing cost. The user can control how the total system gain (Quality Spread Differential) is allocated among all three parties.
Asymmetric Swap (Company Bears FX Risk): An advanced structure where the intermediary operates a “pass-through” on one currency leg, concentrating its profit on the other leg. One company receives a fully-hedged single-currency outcome, while the other company is left with a blended multi-currency obligation and therefore bears foreign exchange risk.
1. Underlying Borrowing Rates
First, set the direct borrowing costs for each company in both currency markets.
US Corp Borrowing Rates
EU Corp Borrowing Rates
2. Swap Structure Selection
Choose the swap structure to explore:
3. Define Swap Parameters
// Conditional UI rendering with proper heading
swapParamsUI = {
if (swapScenario === "standard") {
return htl.html`<div>
<h4 style="margin-top: 0; color: #495057;">Gain Allocation</h4>
<p style="font-size: 0.9rem; color: #6c757d; margin-bottom: 1rem;">
In this fully-hedged structure, both companies receive a single-currency outcome.
Distribute the total system gain among all three parties:
</p>
</div>`;
} else {
return htl.html`<div>
<h4 style="margin-top: 0; color: #495057;">Swap Configuration</h4>
<p style="font-size: 0.9rem; color: #6c757d; margin-bottom: 1rem;">
In this structure, the intermediary takes a pass-through position on one currency leg,
concentrating all profit on the other leg. One company receives a fully-hedged outcome,
while the other bears FX risk with a multi-currency obligation.
</p>
</div>`;
}
}// STANDARD SWAP CONTROLS - Create base inputs (hidden, shown conditionally below)
viewof splitEqually = Inputs.toggle({label: "Split remaining gain equally", value: true})viewof standardIntermediaryGain = Inputs.range([0, Math.max(0, gains.totalGainBps)], {
value: Math.max(0, gains.totalGainBps) * 0.2,
step: 1,
label: "Financial Intermediary gain (bps)"
})viewof standardUsCorpGain = Inputs.range([0, Math.max(0, gains.totalGainBps)], {
value: Math.max(0, gains.totalGainBps) * 0.4,
step: 1,
label: "US Corp gain (bps)"
})viewof standardEuCorpGain = Inputs.range([0, Math.max(0, gains.totalGainBps)], {
value: Math.max(0, gains.totalGainBps) * 0.4,
step: 1,
label: "EU Corp gain (bps)"
})// ASYMMETRIC SWAP CONTROLS
viewof hedgedParty = Inputs.radio(["US Corp", "EU Corp"], {
label: "Fully-hedged party (receives single-currency outcome)",
value: "US Corp"
})viewof hedgedPartyGain = Inputs.range([0, Math.max(0, gains.totalGainBps)], {
value: Math.max(0, gains.totalGainBps) * 0.4,
step: 1,
label: "Fully-hedged party gain (bps)"
})viewof asymmetricIntermediaryGain = Inputs.range([0, Math.max(0, gains.totalGainBps)], {
value: Math.max(0, gains.totalGainBps) * 0.3,
step: 1,
label: "Intermediary gain on profitable leg (bps)"
})// Conditionally display standard swap controls
standardControlsDisplay = {
if (swapScenario === "standard") {
return htl.html`<div>
<div style="margin-bottom: 1rem;">
${viewof splitEqually}
</div>
${splitEqually
? htl.html`<div>${viewof standardIntermediaryGain}</div>`
: htl.html`<div>
<div style="margin-bottom: 0.5rem;">${viewof standardUsCorpGain}</div>
<div>${viewof standardEuCorpGain}</div>
</div>`
}
</div>`;
}
return htl.html``;
}// Conditionally display asymmetric swap controls
asymmetricControlsDisplay = {
if (swapScenario === "asymmetric") {
return htl.html`<div>
<div style="margin-bottom: 1rem;">${viewof hedgedParty}</div>
<div style="margin-bottom: 0.5rem;">${viewof hedgedPartyGain}</div>
<div>${viewof asymmetricIntermediaryGain}</div>
</div>`;
}
return htl.html``;
}// Calculate final gain allocations based on scenario
finalGains = {
const { totalGainBps, usdSpread, eurSpread } = gains;
if (swapScenario === "standard") {
// Standard swap: calculate gains based on user allocation
const usGain = splitEqually
? (Math.max(0, totalGainBps - standardIntermediaryGain) / 2)
: standardUsCorpGain;
const euGain = splitEqually
? (Math.max(0, totalGainBps - standardIntermediaryGain) / 2)
: standardEuCorpGain;
const intGain = totalGainBps - usGain - euGain;
return {
scenario: "standard",
usCorpGainBps: usGain,
euCorpGainBps: euGain,
intermediaryGainBps: intGain,
hedgedParty: null,
riskBearingParty: null
};
} else {
// Asymmetric swap: calculate based on hedged party selection
const hedged = hedgedParty;
const hedgedGain = hedgedPartyGain;
const intGain = asymmetricIntermediaryGain;
// The risk-bearing party's gain is residual
const riskBearingGain = totalGainBps - hedgedGain - intGain;
return {
scenario: "asymmetric",
usCorpGainBps: hedged === "US Corp" ? hedgedGain : riskBearingGain,
euCorpGainBps: hedged === "EU Corp" ? hedgedGain : riskBearingGain,
intermediaryGainBps: intGain,
hedgedParty: hedged,
riskBearingParty: hedged === "US Corp" ? "EU Corp" : "US Corp"
};
}
}// STAGE 3: Calculate all outcomes and generate views
outcomes = {
const { totalGainBps, usdSpread, eurSpread } = gains;
const rateFormat = d3.format(".2f");
const effectiveCostFormat = d3.format(".3f");
const bpsFormat = d3.format(",.1f");
const { scenario, usCorpGainBps, euCorpGainBps, intermediaryGainBps, hedgedParty, riskBearingParty } = finalGains;
// --- Calculate effective rates based on scenario ---
let usEffectiveCostText, euEffectiveCostText, usPaysText, usReceivesText, euPaysText, euReceivesText;
let intermediaryGainText;
let usHasFXRisk = false;
let euHasFXRisk = false;
let usCorpGainDisplay, euCorpGainDisplay, interpretationText;
if (scenario === "standard") {
const usEffectiveEurRate = usCorpRates.eur - (usCorpGainBps / 100);
const euEffectiveUsdRate = euCorpRates.usd - (euCorpGainBps / 100);
usEffectiveCostText = `${effectiveCostFormat(usEffectiveEurRate)}% (EUR)`;
euEffectiveCostText = `${effectiveCostFormat(euEffectiveUsdRate)}% (USD)`;
usPaysText = usEffectiveCostText;
usReceivesText = `${rateFormat(usCorpRates.usd)}% (USD)`;
euPaysText = euEffectiveCostText;
euReceivesText = `${rateFormat(euCorpRates.eur)}% (EUR)`;
const intGainUsdBps = (usdSpread * 100) - euCorpGainBps;
const intGainEurBps = (-eurSpread * 100) - usCorpGainBps;
intermediaryGainText = `${bpsFormat(intGainUsdBps)} bps (USD) + ${bpsFormat(intGainEurBps)} bps (EUR)`;
usCorpGainDisplay = htl.html`<b>${bpsFormat(usCorpGainBps)} bps</b>`;
euCorpGainDisplay = htl.html`<b>${bpsFormat(euCorpGainBps)} bps</b>`;
interpretationText = htl.html`<p>In this fully-hedged structure, both corporations have successfully transformed the currency of their debt while achieving a lower interest rate than they could have obtained directly. The intermediary absorbs all foreign exchange risk, and in return, earns a spread. The final "Effective Cost" for each company is a fixed, single-currency obligation, providing them with certainty about their future financing costs.</p>`;
} else { // Asymmetric scenario
if (hedgedParty === "US Corp") {
usHasFXRisk = false;
euHasFXRisk = true;
const usEffectiveEurRate = usCorpRates.eur - (usCorpGainBps / 100);
usEffectiveCostText = `${effectiveCostFormat(usEffectiveEurRate)}% (EUR)`;
usPaysText = usEffectiveCostText;
usReceivesText = `${rateFormat(usCorpRates.usd)}% (USD)`;
const int_receives_eur_from_us = usEffectiveEurRate;
const int_pays_eur_to_eu = int_receives_eur_from_us - (intermediaryGainBps / 100);
const int_receives_usd_from_eu = usCorpRates.usd;
euPaysText = `${rateFormat(int_receives_usd_from_eu)}% (USD)`;
euReceivesText = `${rateFormat(int_pays_eur_to_eu)}% (EUR)`;
const eu_final_usd_cost = int_receives_usd_from_eu;
const eu_final_eur_cost = euCorpRates.eur - int_pays_eur_to_eu;
euEffectiveCostText = `${effectiveCostFormat(eu_final_usd_cost)}% (USD) & ${effectiveCostFormat(eu_final_eur_cost)}% (EUR)`;
intermediaryGainText = `${bpsFormat(intermediaryGainBps)} bps (on EUR leg) & 0 bps (on USD leg)`;
usCorpGainDisplay = htl.html`<b>${bpsFormat(usCorpGainBps)} bps</b>`;
const usdLegGainBps = (euCorpRates.usd - eu_final_usd_cost) * 100;
const eurLegLossBps = -eu_final_eur_cost * 100;
euCorpGainDisplay = htl.html`<b>${bpsFormat(euCorpGainBps)} bps</b><br><small style="font-weight: normal; color: #495057;">(${usdLegGainBps >= 0 ? '+' : ''}${bpsFormat(usdLegGainBps)} bps USD & ${eurLegLossBps >= 0 ? '+' : ''}${bpsFormat(eurLegLossBps)} bps EUR)</small>`;
} else { // hedgedParty === "EU Corp"
usHasFXRisk = true;
euHasFXRisk = false;
const euEffectiveUsdRate = euCorpRates.usd - (euCorpGainBps / 100);
euEffectiveCostText = `${effectiveCostFormat(euEffectiveUsdRate)}% (USD)`;
euPaysText = euEffectiveCostText;
euReceivesText = `${rateFormat(euCorpRates.eur)}% (EUR)`;
const int_receives_usd_from_eu = euEffectiveUsdRate;
const int_pays_usd_to_us = int_receives_usd_from_eu - (intermediaryGainBps / 100);
const int_receives_eur_from_us = euCorpRates.eur;
usPaysText = `${rateFormat(int_receives_eur_from_us)}% (EUR)`;
usReceivesText = `${rateFormat(int_pays_usd_to_us)}% (USD)`;
const us_final_eur_cost = int_receives_eur_from_us;
const us_final_usd_cost = usCorpRates.usd - int_pays_usd_to_us;
usEffectiveCostText = `${effectiveCostFormat(us_final_usd_cost)}% (USD) & ${effectiveCostFormat(us_final_eur_cost)}% (EUR)`;
intermediaryGainText = `${bpsFormat(intermediaryGainBps)} bps (on USD leg) & 0 bps (on EUR leg)`;
euCorpGainDisplay = htl.html`<b>${bpsFormat(euCorpGainBps)} bps</b>`;
const eurLegGainBps = (usCorpRates.eur - us_final_eur_cost) * 100;
const usdLegGainBps = -us_final_usd_cost * 100;
usCorpGainDisplay = htl.html`<b>${bpsFormat(usCorpGainBps)} bps</b><br><small style="font-weight: normal; color: #495057;">(${usdLegGainBps >= 0 ? '+' : ''}${bpsFormat(usdLegGainBps)} bps USD & ${eurLegGainBps >= 0 ? '+' : ''}${bpsFormat(eurLegGainBps)} bps EUR)</small>`;
}
interpretationText = htl.html`<div>
<p>This structure demonstrates a critical trade-off between cost-saving and risk.</p>
<ul>
<li>The <b>hedged party</b> (${hedgedParty}) achieves a guaranteed, single-currency borrowing cost that is lower than its direct alternative. It is completely insulated from foreign exchange risk.</li>
<li>The <b>risk-bearing party</b> (${riskBearingParty}) secures a portion of the swap's gains but is left with a synthetic, multi-currency liability. Its final financing cost is not fixed; it is now a blend of USD and EUR obligations. The ultimate cost of this position will fluctuate with the USD/EUR exchange rate, creating both the potential for further gains or unexpected losses. This exposure is the price it pays for its share of the swap's benefits.</li>
</ul>
</div>`;
}
// --- BUILD HTML VIEWS ---
const gainDisplay = (() => {
if (totalGainBps < 0.1) {
return htl.html`<div class="no-gain-card">
<div class="no-gain-header">No Net System Gain</div>
<div class="no-gain-value">${bpsFormat(totalGainBps)} bps</div>
<p class="no-gain-explanation">The borrowing spreads are nearly identical in both currency markets.</p>
</div>`;
}
const gainExplanation = `This gain arises because the borrowing spread in the USD market (${rateFormat(usdSpread*100)} bps) differs from the spread in the EUR market (${rateFormat(eurSpread*100)} bps).`;
return htl.html`<div class="gain-card">
<div class="gain-header">Net System Gain (QSD)</div>
<div class="gain-value">${bpsFormat(totalGainBps)} basis points</div>
<p class="gain-explanation">${gainExplanation}</p>
</div>`;
})();
const allocationWarning = (intermediaryGainBps < -0.1)
? htl.html`<div class="warning-card">⚠️ The allocated gains exceed the total available gain; the intermediary has a loss.</div>`
: null;
const usCorpBorrowsText = `${rateFormat(usCorpRates.usd)}% (USD)`;
const euCorpBorrowsText = `${rateFormat(euCorpRates.eur)}% (EUR)`;
const principalStartDiagram = htl.html`<div class="diagram-card"><h3>Principal Exchange at Inception</h3><svg viewBox="0 0 1000 180" style="width: 100%; max-width: 1000px; height: auto; margin: 0 auto; display: block;">
<defs>
<marker id="arrowBlue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#1f77b4" /></marker>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#d62728" /></marker>
</defs>
<text x="50" y="95" font-size="15" font-weight="600" text-anchor="middle">US Corp</text>
<line x1="110" y1="85" x2="330" y2="85" stroke="#1f77b4" stroke-width="2.5" marker-end="url(#arrowBlue)"/><text x="220" y="78" font-size="13" fill="#1f77b4" text-anchor="middle">Principal (USD)</text>
<line x1="330" y1="105" x2="110" y2="105" stroke="#d62728" stroke-width="2.5" marker-end="url(#arrowRed)"/><text x="220" y="122" font-size="13" fill="#d62728" text-anchor="middle">Principal (EUR)</text>
<rect x="330" y="70" width="150" height="50" fill="#f8f9fa" stroke="#495057" stroke-width="2" rx="5"/><text x="405" y="92" font-size="14" font-weight="600" text-anchor="middle">Financial</text><text x="405" y="109" font-size="14" font-weight="600" text-anchor="middle">Intermediary</text>
<line x1="480" y1="85" x2="740" y2="85" stroke="#1f77b4" stroke-width="2.5" marker-end="url(#arrowBlue)"/><text x="610" y="78" font-size="13" fill="#1f77b4" text-anchor="middle">Principal (USD)</text>
<line x1="740" y1="105" x2="480" y2="105" stroke="#d62728" stroke-width="2.5" marker-end="url(#arrowRed)"/><text x="610" y="122" font-size="13" fill="#d62728" text-anchor="middle">Principal (EUR)</text>
<text x="800" y="95" font-size="15" font-weight="600" text-anchor="middle">EU Corp</text>
</svg></div>`;
const principalEndDiagram = htl.html`<div class="diagram-card"><h3>Principal Exchange at Maturity</h3><svg viewBox="0 0 1000 180" style="width: 100%; max-width: 1000px; height: auto; margin: 0 auto; display: block;">
<defs>
<marker id="arrowBlue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#1f77b4" /></marker>
<marker id="arrowRed" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#d62728" /></marker>
</defs>
<text x="50" y="95" font-size="15" font-weight="600" text-anchor="middle">US Corp</text>
<line x1="330" y1="85" x2="110" y2="85" stroke="#1f77b4" stroke-width="2.5" marker-end="url(#arrowBlue)"/><text x="220" y="78" font-size="13" fill="#1f77b4" text-anchor="middle">Principal (USD)</text>
<line x1="110" y1="105" x2="330" y2="105" stroke="#d62728" stroke-width="2.5" marker-end="url(#arrowRed)"/><text x="220" y="122" font-size="13" fill="#d62728" text-anchor="middle">Principal (EUR)</text>
<rect x="330" y="70" width="150" height="50" fill="#f8f9fa" stroke="#495057" stroke-width="2" rx="5"/><text x="405" y="92" font-size="14" font-weight="600" text-anchor="middle">Financial</text><text x="405" y="109" font-size="14" font-weight="600" text-anchor="middle">Intermediary</text>
<line x1="480" y1="105" x2="740" y2="105" stroke="#d62728" stroke-width="2.5" marker-end="url(#arrowRed)"/><text x="610" y="122" font-size="13" fill="#d62728" text-anchor="middle">Principal (EUR)</text>
<line x1="740" y1="85" x2="480" y2="85" stroke="#1f77b4" stroke-width="2.5" marker-end="url(#arrowBlue)"/><text x="610" y="78" font-size="13" fill="#1f77b4" text-anchor="middle">Principal (USD)</text>
<text x="800" y="95" font-size="15" font-weight="600" text-anchor="middle">EU Corp</text>
</svg></div>`;
const swapDiagram = htl.html`<div class="diagram-card"><h3>Interest Payment Flows</h3><svg viewBox="0 0 1000 180" style="width: 100%; max-width: 1000px; height: auto; margin: 0 auto; display: block;">
<defs>
<marker id="arrowBlack" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#333" /></marker>
<marker id="arrowGreen" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#2ca02c" /></marker>
<marker id="arrowOrange" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto"><polygon points="0 0, 10 3, 0 6" fill="#ff7f0e" /></marker>
</defs>
<text x="50" y="95" font-size="15" font-weight="600" text-anchor="middle">US Corp</text>
<line x1="50" y1="75" x2="50" y2="25" stroke="#333" stroke-width="2.5" marker-end="url(#arrowBlack)"/><text x="60" y="50" font-size="13" fill="#333">${usCorpBorrowsText}</text>
<line x1="110" y1="85" x2="330" y2="85" stroke="#2ca02c" stroke-width="2.5" marker-end="url(#arrowGreen)"/><text x="220" y="78" font-size="13" fill="#2ca02c" text-anchor="middle">${usPaysText}</text>
<line x1="330" y1="105" x2="110" y2="105" stroke="#ff7f0e" stroke-width="2.5" marker-end="url(#arrowOrange)"/><text x="220" y="122" font-size="13" fill="#ff7f0e" text-anchor="middle">${usReceivesText}</text>
<rect x="330" y="70" width="150" height="50" fill="#f8f9fa" stroke="#495057" stroke-width="2" rx="5"/><text x="405" y="92" font-size="14" font-weight="600" text-anchor="middle">Financial</text><text x="405" y="109" font-size="14" font-weight="600" text-anchor="middle">Intermediary</text>
<line x1="480" y1="85" x2="740" y2="85" stroke="#ff7f0e" stroke-width="2.5" marker-end="url(#arrowOrange)"/><text x="610" y="78" font-size="13" fill="#ff7f0e" text-anchor="middle">${euReceivesText}</text>
<line x1="740" y1="105" x2="480" y2="105" stroke="#2ca02c" stroke-width="2.5" marker-end="url(#arrowGreen)"/><text x="610" y="122" font-size="13" fill="#2ca02c" text-anchor="middle">${euPaysText}</text>
<text x="800" y="95" font-size="15" font-weight="600" text-anchor="middle">EU Corp</text>
<line x1="800" y1="75" x2="800" y2="25" stroke="#333" stroke-width="2.5" marker-end="url(#arrowBlack)"/><text x="815" y="50" font-size="13" fill="#333">${euCorpBorrowsText}</text>
</svg></div>`;
const totalAllocatedGain = usCorpGainBps + euCorpGainBps + intermediaryGainBps;
const resultsTable = htl.html`<div class="results-table-wrapper">
<h3>All-in Costs and Gains</h3>
<table>
<thead>
<tr>
<th>Party</th>
<th>Direct Cost (USD)</th>
<th>Direct Cost (EUR)</th>
<th>Effective Cost with Swap</th>
<th>Gain</th>
${scenario === "asymmetric" ? htl.html`<th>FX Risk</th>` : ""}
</tr>
</thead>
<tbody>
<tr style="${usHasFXRisk ? 'background-color: #fff3cd;' : ''}">
<td><b>US Corp</b></td>
<td>${rateFormat(usCorpRates.usd)}%</td>
<td>${rateFormat(usCorpRates.eur)}%</td>
<td><b>${usEffectiveCostText}</b></td>
<td>${usCorpGainDisplay}</td>
${scenario === "asymmetric" ? htl.html`<td>${usHasFXRisk ? htl.html`<span style="color: #856404;">⚠️ Exposed</span>` : '✓ Hedged'}</td>` : ""}
</tr>
<tr style="${euHasFXRisk ? 'background-color: #fff3cd;' : ''}">
<td><b>EU Corp</b></td>
<td>${rateFormat(euCorpRates.usd)}%</td>
<td>${rateFormat(euCorpRates.eur)}%</td>
<td><b>${euEffectiveCostText}</b></td>
<td>${euCorpGainDisplay}</td>
${scenario === "asymmetric" ? htl.html`<td>${euHasFXRisk ? htl.html`<span style="color: #856404;">⚠️ Exposed</span>` : '✓ Hedged'}</td>` : ""}
</tr>
<tr>
<td><b>Intermediary</b></td>
<td>—</td>
<td>—</td>
<td>${intermediaryGainText}</td>
<td><b>${bpsFormat(intermediaryGainBps)} bps</b></td>
${scenario === "asymmetric" ? htl.html`<td>—</td>` : ""}
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4" style="text-align: right; font-weight: bold;">Total Allocated Gain</td>
<td><b>${bpsFormat(totalAllocatedGain)} bps</b></td>
${scenario === "asymmetric" ? htl.html`<td></td>` : ""}
</tr>
</tfoot>
</table>
</div>`;
return { gainDisplay, allocationWarning, principalStartDiagram, swapDiagram, principalEndDiagram, resultsTable, interpretationText };
}