diff --git a/20-FX-Vol-Analytics/README.md b/20-FX-Vol-Analytics/README.md
new file mode 100644
index 000000000..430c25553
--- /dev/null
+++ b/20-FX-Vol-Analytics/README.md
@@ -0,0 +1,82 @@
+# FX Volatility Trading Analytics Prototype
+
+## P&L Delta-Hedging Mockup (Call, 1-Day Tenor)
+Using the required long-option perspective and r = 0, the example below follows a 100 million USD notional EURUSD call with strike 1.1000, initial spot 1.1000, **constant** implied volatility 10% (annualised), tenor 24 hours, and hedging every six hours. Spot path (in USD per EUR): 1.1000 → 1.1050 → 1.0950 → 1.1020 → 1.1070.
+
+| Time (h) | Spot | Tau (yrs) | σ (kept at 10%) | Delta | Hedge action (Δ) | Hedge P&L vs expiry (USD) | Cum. hedge P&L (USD) |
+|---------:|:------|:----------|:----------------|:------|:-----------------|--------------------------:|---------------------:|
+| 0 | 1.1000 | 0.002740 | 0.1000 | 0.5010 | Sell 0.5010 × 100m | -318,846.23 | -318,846.23 |
+| 6 | 1.1050 | 0.002055 | 0.1000 | 0.8420 | Sell 0.3410 × 100m | -61,712.95 | -380,559.18 |
+| 12 | 1.0950 | 0.001370 | 0.1000 | 0.1095 | Buy 0.7325 × 100m | +802,722.31 | +422,163.12 |
+| 18 | 1.1020 | 0.000685 | 0.1000 | 0.7566 | Sell 0.6471 × 100m | -293,591.15 | +128,571.97 |
+| 24 | 1.1070 | 0 | – | 1.0000 | Buy 0.2434 × 100m | 0.00 | +128,571.97 |
+
+- **Premium paid at t=0:** 0.00229697 × 100m = 229,697.26 USD
+- **Hedge gains/losses:** Sum of expiry-referenced hedge P&Ls (–318,846.23 – 61,712.95 + 802,722.31 – 293,591.15) = +128,571.97 USD
+- **Intrinsic value at expiry:** max(1.1070 – 1.1000, 0) × 100m = 700,000.00 USD
+- **Total P&L:** -Premium + Hedge P&L + Intrinsic = -229,697.26 + 128,571.97 + 700,000.00 = **+598,874.71 USD**
+
+For path-dependent monitoring before expiry you can still track `P&L(t) = -Premium_paid + Cumulative_hedge_P&L(t) + Option_MTM(t)` using the running delta-hedge P&L and option mark-to-market at each hedge timestamp.
+
+### Realised Volatility Example
+Using the same four six-hour log returns, the **sum of squared returns** is 0.000164313. For a one-day horizon (24 hours ≈ 1/365 years) the annualised realised volatility is:
+
+```
+σ_realised = sqrt((∑ r_i²) × 365) = 24.49%
+```
+
+This matches the standard realised-variance definition `∑ r_i² / Δt_years` where `Δt_years = 1/365` for a 24-hour sample.
+
+### Notional Standardisation Logic
+- Quote currency is USD (e.g. EURUSD, GBPUSD): **use 100 million USD notional**; P&L reported in USD.
+- Base currency is USD (e.g. USDJPY, USDCHF): **use 100 million units of the quote currency** (JPY, CHF, etc.). P&L is produced in that quote currency and displayed as unitless for cross-comparison.
+- Cross pairs without USD (e.g. AUDCHF, EURJPY): **use 100 million units of the quote currency**. Calculations occur in that currency, results shown as unitless multiples of the 100m-equivalent notionals.
+
+These rules align every trade near a 100 million USD exposure so that P&L figures remain comparable across currency pairs.
+
+---
+
+The remainder of this README explains how to run the in-browser prototype once built.
+
+## Running the Prototype
+1. Open `index.html` in any modern browser (Chrome, Edge, Firefox, Safari). No server is required—the app runs entirely in the browser.
+2. Upload the spot time-series workbook:
+ - First sheet only is processed.
+ - Include a column named `timestamp` (Excel datetime or ISO string) and one column per currency pair (e.g. `EURUSD`, `USDJPY`).
+ - Provide at least the time range that covers the tenors you wish to analyse.
+3. Upload the implied volatility workbook:
+ - First sheet only is processed.
+ - Required columns (case-insensitive): `pair`, `option_type` (`Call`/`Put`), `tenor_hours` (or `tenor_days`), `strike_label`, `implied_vol` (decimal, 0.10 = 10%).
+ - `strike_label` is the bucket shown in the heatmap/ranking (e.g. `ATM`, `25D Call`, `10D Put`). If the column is left blank the app auto-labels using the provided strike/delta: `ATM` for ~50Δ, `25ΔC`/`25ΔP` for delta buckets, or `K=1.1050` when only an absolute strike exists.
+ - Optional: `strike` (explicit strike) or `delta_target` (to back out the strike).
+ - Any additional commentary columns are ignored.
+4. Choose the currency pairs and strike buckets to analyse (leave empty to take all), select hedging frequency (10, 30, or 60 minutes), and pick the variance decay model.
+5. Click **Run P&L Simulation** to generate the dashboard. Click any P&L cell to view its path-decomposition chart.
+6. Download the Excel report for the full hedge-by-hedge breakdown and summary table.
+
+### Variance Decay Options
+- **Standard √t decay:** σ(t) = σ₀ × √(t_remaining / t_total).
+- **Flat:** Keeps σ constant throughout the life of the trade.
+- **Event-weighted window:** Allocate a chosen share of total variance to a specific hour-window (e.g. central-bank announcement). The remaining variance is distributed proportionally outside the window; the engine recomputes σ(t) from the residual variance budget.
+
+### Output Overview
+- **P&L heatmap:** Currency vs strike/tenor grid colour-coded by total P&L (per 100m notional).
+- **P&L rankings:** Top and bottom five trades.
+- **Volatility comparison:** Implied vs realised σ, with flags where P&L contradicts the vol differential (e.g. realised > implied but trade loses money).
+- **Interactive chart:** Premium, hedge P&L, option MTM, and total P&L lines over time.
+- **Excel export:** `Summary` (per-trade metrics) and `HedgePaths` (hedge-by-hedge records with timestamps, deltas, MTM, and cumulative P&L).
+
+### Notes and Assumptions
+- Risk-free and foreign rates are fixed at zero, matching the short-dated setup.
+- Hedging is discrete at the chosen frequency and ignores transaction costs.
+- Spot prices are assumed to be clean post any Monday-market filtering; pre-processing should remove stale values.
+- If spot data ends before a tenor expires, that trade is flagged and excluded.
+- All P&L values are shown both in the underlying currency (per the notional convention) and implicitly as a unitless number by dividing by 100 million.
+- The browser loads SheetJS' `xlsx.full.min.js` automatically. If your environment blocks CDNs, download that file and place it alongside `index.html` so the local fallback is picked up.
+
+### Future Enhancements
+- Incorporate transaction costs and slippage controls.
+- Allow custom strikes in delta or absolute terms directly inside the UI.
+- Persist historical runs for longitudinal analysis.
+- Add gamma-weighted realised volatility metrics.
+- Connect to upstream APIs (Bloomberg, internal data lakes) once permissions are available.
diff --git a/20-FX-Vol-Analytics/index.html b/20-FX-Vol-Analytics/index.html
new file mode 100644
index 000000000..1176e5a56
--- /dev/null
+++ b/20-FX-Vol-Analytics/index.html
@@ -0,0 +1,2131 @@
+
+
+
+
+ FX Volatility P&L Analytics
+
+
+
+
+
+
+
+
+
+
+ FX Volatility P&L Analytics
+ This single-page tool simulates the P&L of buying FX options (always long) and delta-hedging them at configurable frequencies, using zero rates and a normalised 100 million notional convention. Upload Bloomberg-style spot time series (10–30 minute sampling) and implied volatility surfaces (strike/tenor) to analyse which structures deliver positive economic P&L.
+ Input expectations, the worked P&L example, and notional logic are documented in README.md beside this file.
+
+
+
+ 1. Upload Data
+ Loading Excel parser…
+
+
+
Spot time series (Excel)
+
+
Awaiting upload. Columns: timestamp , PAIR1 , …
+
+
+
Implied vols & strikes (Excel)
+
+
Awaiting upload. Columns: pair , tenor_hours (or tenor_days), option_type , strike_label (e.g. ATM, 25D Call; leave blank to auto-label), implied_vol , optional strike /delta_target .
+
+
+
+
+
+ 2. Configure Simulation
+
+
+
Currency pairs
+
+
Pairs populated after loading both files. Hold Ctrl/Cmd to select several. Leave empty for all.
+
+
+
Strike buckets
+
+
e.g. ATM, 25D Call, 10D Put. Leave empty to include every strike label.
+
+
+ Hedge every (minutes)
+
+ 10 minutes
+ 30 minutes
+ 60 minutes
+
+
+
+
+ Variance Decay
+
+ Standard √t decay
+ Flat (no decay)
+ Event-weighted window
+
+
+
+ Event window start (hours from t₀)
+
+
+
+ Event window end (hours from t₀)
+
+
+
+
Variance share in window (0-1)
+
+
Remaining variance is distributed proportionally before/after the event.
+
+
+ Run P&L Simulation
+
+
+
+
+
3. P&L Dashboard
+ Download Excel Report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ FX Volatility P&L Analytics
+ This single-page tool simulates the P&L of buying FX options (always long) and delta-hedging them at configurable frequencies, using zero rates and a normalised 100 million notional convention. Upload Bloomberg-style spot time series (10–30 minute sampling) and implied volatility surfaces (strike/tenor) to analyse which structures deliver positive economic P&L.
+ Input expectations, the worked P&L example, and notional logic are documented in README.md beside this file.
+
+
+
+ 1. Upload Data
+ Loading Excel parser…
+
+
+
Spot time series (Excel)
+
+
Awaiting upload. Columns: timestamp , PAIR1 , …
+
+
+
Implied vols & strikes (Excel)
+
+
Awaiting upload. Columns: pair , tenor_hours (or tenor_days), option_type , strike_label (e.g. ATM, 25D Call; leave blank to auto-label), implied_vol , optional strike /delta_target .
+
+
+
+
+
+ 2. Configure Simulation
+
+
+
Currency pairs
+
+
Pairs populated after loading both files. Hold Ctrl/Cmd to select several. Leave empty for all.
+
+
+
Strike buckets
+
+
e.g. ATM, 25D Call, 10D Put. Leave empty to include every strike label.
+
+
+ Hedge every (minutes)
+
+ 10 minutes
+ 30 minutes
+ 60 minutes
+
+
+
+
+ Variance Decay
+
+ Standard √t decay
+ Flat (no decay)
+ Event-weighted window
+
+
+
+ Event window start (hours from t₀)
+
+
+
+ Event window end (hours from t₀)
+
+
+
+
Variance share in window (0-1)
+
+
Remaining variance is distributed proportionally before/after the event.
+
+
+ Run P&L Simulation
+
+
+
+
+
3. P&L Dashboard
+ Download Excel Report
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fx_option_pnl_calculator.html b/fx_option_pnl_calculator.html
new file mode 100644
index 000000000..d0cd2c140
--- /dev/null
+++ b/fx_option_pnl_calculator.html
@@ -0,0 +1,2671 @@
+
+
+
+
+
+ FX Option P&L Calculator
+
+
+
+
+
+
+
+
FX Option P&L Calculator
+
+
+
+ Single Pair Analysis
+ Batch Analysis
+
+
+
+
+
+ Configuration: EURUSD Options (5 strikes), Notional: 100M Settlement Currency (USD) per strike
+ Note: Base currency (EUR) notional varies by strike: Base Notional = 100M / Strike
+ Data Format: Variable-length spot series, 10-minute sampling intervals
+ Model: Black-Scholes with r=0, q=0
+ Strikes: 10Δ Put, 25Δ Put, ATM Call, 25Δ Call, 10Δ Call
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Batch Analysis - Multiple Currency Pairs
+
+
+
+
Step 1: Upload Excel File
+
+
+
File Requirements:
+
+ Excel file (.xlsx) with two sheets: "Spot Data" and "Implied Vols"
+ Spot Data: Timestamp column + currency pair columns
+ Implied Vols: Pair names + ImpliedVol (as decimals, e.g., 0.105 = 10.5%)
+
+
+
+
+
+
+
+
+
+
+
+
+
Step 2: Select Currency Pairs to Analyze
+
+ Select All
+ Deselect All
+ Selected: 0 pairs
+
+
+
+
+
Calculate All Selected Pairs
+
+
+
+
+
+
+
+
Results
+
+
+
+
TOP 5 PERFORMERS
+
+
+
BOTTOM 5 PERFORMERS
+
+
+
+
+
+
+
+
+
Pair Details (Click to Expand)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test_calc_logic.js b/test_calc_logic.js
new file mode 100644
index 000000000..d3a96a836
--- /dev/null
+++ b/test_calc_logic.js
@@ -0,0 +1,61 @@
+// Test the calculator logic
+const SETTLEMENT_NOTIONAL = 100_000_000;
+const NUM_POINTS = 145;
+
+// Simulate creating test data
+const spots = [];
+let currentSpot = 1.1000;
+for (let i = 0; i < NUM_POINTS; i++) {
+ spots.push(currentSpot);
+ currentSpot += (Math.random() - 0.5) * 0.0001;
+}
+
+console.log("Generated", spots.length, "spot prices");
+console.log("First spot:", spots[0]);
+console.log("Last spot:", spots[spots.length - 1]);
+
+// Simulate strikes calculation
+const strikes = [
+ { name: '10Δ Put', strike: 1.0926, type: 'put' },
+ { name: '25Δ Put', strike: 1.0960, type: 'put' },
+ { name: 'ATM Call', strike: 1.1000, type: 'call' },
+ { name: '25Δ Call', strike: 1.1040, type: 'call' },
+ { name: '10Δ Call', strike: 1.1074, type: 'call' }
+];
+
+console.log("\nSimulating allResults structure:");
+const allResults = strikes.map((s, idx) => {
+ const baseNotional = SETTLEMENT_NOTIONAL / s.strike;
+ console.log(`Strike ${idx + 1}: ${s.name}, base notional: ${(baseNotional / 1_000_000).toFixed(1)}M`);
+
+ // Simulate results array with simple mock data
+ const results = [];
+ for (let i = 0; i < NUM_POINTS; i++) {
+ results.push({
+ time: i * 10,
+ spot: spots[i],
+ delta: 0.5,
+ portfolioValue: 1000 * i
+ });
+ }
+
+ return {
+ name: s.name,
+ strike: s.strike,
+ type: s.type,
+ baseNotional: baseNotional,
+ results: results
+ };
+});
+
+console.log("\nallResults created with", allResults.length, "strikes");
+console.log("Each strike has", allResults[0].results.length, "time points");
+
+// Simulate accessing data like displayResults does
+console.log("\nSimulating displayResults access pattern:");
+allResults.forEach((r, idx) => {
+ const finalPnL = r.results[r.results.length - 1].portfolioValue;
+ console.log(`Strike ${idx + 1}: ${r.name}, final P&L: ${finalPnL}`);
+});
+
+console.log("\nAll simulation checks passed!");
diff --git a/test_calculator.html b/test_calculator.html
new file mode 100644
index 000000000..2ab296163
--- /dev/null
+++ b/test_calculator.html
@@ -0,0 +1,23 @@
+
+
+
+ Test
+
+
+ Test
+
+
+
+
diff --git a/test_custom_strikes.js b/test_custom_strikes.js
new file mode 100644
index 000000000..ecb3a0450
--- /dev/null
+++ b/test_custom_strikes.js
@@ -0,0 +1,126 @@
+// Test custom strike functionality
+
+console.log("=== Testing Custom Strike Logic ===\n");
+
+const initialSpot = 1.1000;
+
+console.log("Initial spot:", initialSpot);
+console.log("");
+
+// Test 1: Strike Price mode - OTM Put
+console.log("Test 1: Strike Price = 1.0850 (below spot)");
+const strike1 = 1.0850;
+const optionType1 = strike1 < initialSpot ? 'put' : 'call';
+console.log(" Strike:", strike1);
+console.log(" Auto-determined type:", optionType1);
+console.log(" Expected: Put (since 1.0850 < 1.1000)");
+console.log(" Result:", optionType1 === 'put' ? "✓" : "✗");
+console.log("");
+
+// Test 2: Strike Price mode - OTM Call
+console.log("Test 2: Strike Price = 1.1250 (above spot)");
+const strike2 = 1.1250;
+const optionType2 = strike2 < initialSpot ? 'put' : 'call';
+console.log(" Strike:", strike2);
+console.log(" Auto-determined type:", optionType2);
+console.log(" Expected: Call (since 1.1250 > 1.1000)");
+console.log(" Result:", optionType2 === 'call' ? "✓" : "✗");
+console.log("");
+
+// Test 3: Strike Price mode - ATM
+console.log("Test 3: Strike Price = 1.1000 (at spot)");
+const strike3 = 1.1000;
+const optionType3 = strike3 < initialSpot ? 'put' : 'call';
+console.log(" Strike:", strike3);
+console.log(" Auto-determined type:", optionType3);
+console.log(" Expected: Call (ATM convention: >= spot is call)");
+console.log(" Result:", optionType3 === 'call' ? "✓" : "✗");
+console.log("");
+
+// Test 4: Delta mode - 15 delta Call
+console.log("Test 4: Delta mode - 15Δ Call");
+const deltaInput4 = 15;
+const deltaDecimal4 = deltaInput4 / 100;
+const userType4 = 'call';
+const targetDelta4 = userType4 === 'put' ? -deltaDecimal4 : deltaDecimal4;
+console.log(" Input delta:", deltaInput4);
+console.log(" Delta decimal:", deltaDecimal4);
+console.log(" User selected:", userType4);
+console.log(" Target delta for calculation:", targetDelta4);
+console.log(" Expected: +0.15");
+console.log(" Result:", targetDelta4 === 0.15 ? "✓" : "✗");
+console.log("");
+
+// Test 5: Delta mode - 35 delta Put
+console.log("Test 5: Delta mode - 35Δ Put");
+const deltaInput5 = 35;
+const deltaDecimal5 = deltaInput5 / 100;
+const userType5 = 'put';
+const targetDelta5 = userType5 === 'put' ? -deltaDecimal5 : deltaDecimal5;
+console.log(" Input delta:", deltaInput5);
+console.log(" Delta decimal:", deltaDecimal5);
+console.log(" User selected:", userType5);
+console.log(" Target delta for calculation:", targetDelta5);
+console.log(" Expected: -0.35");
+console.log(" Result:", targetDelta5 === -0.35 ? "✓" : "✗");
+console.log("");
+
+// Test 6: Delta mode - 50 delta Call (ATM)
+console.log("Test 6: Delta mode - 50Δ Call (ATM)");
+const deltaInput6 = 50;
+const deltaDecimal6 = deltaInput6 / 100;
+const userType6 = 'call';
+const targetDelta6 = userType6 === 'put' ? -deltaDecimal6 : deltaDecimal6;
+console.log(" Input delta:", deltaInput6);
+console.log(" Delta decimal:", deltaDecimal6);
+console.log(" User selected:", userType6);
+console.log(" Target delta for calculation:", targetDelta6);
+console.log(" Expected: +0.50 (ATM)");
+console.log(" Result:", targetDelta6 === 0.50 ? "✓" : "✗");
+console.log("");
+
+// Test 7: Validation - Delta out of range
+console.log("Test 7: Validation - Delta = 105 (invalid)");
+const deltaInput7 = 105;
+const isValid7 = deltaInput7 >= 0 && deltaInput7 <= 100;
+console.log(" Input delta:", deltaInput7);
+console.log(" Valid range: 0-100");
+console.log(" Is valid:", isValid7);
+console.log(" Expected: false");
+console.log(" Result:", !isValid7 ? "✓" : "✗");
+console.log("");
+
+// Test 8: Validation - Delta = 0 (valid edge case)
+console.log("Test 8: Validation - Delta = 0 (valid edge case)");
+const deltaInput8 = 0;
+const isValid8 = deltaInput8 >= 0 && deltaInput8 <= 100;
+console.log(" Input delta:", deltaInput8);
+console.log(" Valid range: 0-100");
+console.log(" Is valid:", isValid8);
+console.log(" Expected: true");
+console.log(" Result:", isValid8 ? "✓" : "✗");
+console.log("");
+
+// Test 9: Validation - Delta = 100 (valid edge case)
+console.log("Test 9: Validation - Delta = 100 (valid edge case)");
+const deltaInput9 = 100;
+const isValid9 = deltaInput9 >= 0 && deltaInput9 <= 100;
+console.log(" Input delta:", deltaInput9);
+console.log(" Valid range: 0-100");
+console.log(" Is valid:", isValid9);
+console.log(" Expected: true");
+console.log(" Result:", isValid9 ? "✓" : "✗");
+console.log("");
+
+// Test 10: Label formatting
+console.log("Test 10: Label formatting");
+console.log(" Strike Price mode (1.0850, Put):");
+const label10a = `Custom: ${1.0850.toFixed(4)} Put`;
+console.log(" " + label10a);
+console.log("");
+console.log(" Delta mode (15Δ Call, strike=1.1065):");
+const label10b = `Custom: 15Δ Call (Strike: ${1.1065.toFixed(4)})`;
+console.log(" " + label10b);
+console.log("");
+
+console.log("=== All Tests Complete ===");
diff --git a/test_hedge_pnl_mtm_fix.js b/test_hedge_pnl_mtm_fix.js
new file mode 100644
index 000000000..2d9b0d1b8
--- /dev/null
+++ b/test_hedge_pnl_mtm_fix.js
@@ -0,0 +1,97 @@
+// Test hedge P&L mark-to-market fix for misaligned final time
+
+console.log("=== Testing Hedge P&L Mark-to-Market at Final Time ===\n");
+
+// Scenario: Base=10min, Hedge=60min, 8 points (70min total)
+// Spots: [1.1000, 1.1030, 1.1020, 1.1050, 1.1060, 1.1070, 1.1060, 1.1050]
+// Hedge times: [0, 60]
+// Final time: 70 (not a hedge time)
+
+const spots = [1.1000, 1.1030, 1.1020, 1.1050, 1.1060, 1.1070, 1.1060, 1.1050];
+
+console.log("Setup:");
+console.log(" Base frequency: 10 minutes");
+console.log(" Hedge frequency: 60 minutes");
+console.log(" Total time: 70 minutes");
+console.log(" Spot prices:", spots);
+console.log("");
+
+// Simplified calculation (assume ATM call for demonstration)
+console.log("Hedge Activity:");
+console.log("");
+
+console.log("t=0: Spot = 1.1000");
+console.log(" Initial hedge: Assume -50M (short 50M)");
+console.log(" Cumulative hedge: -50M");
+console.log(" Weighted avg spot: 1.1000");
+console.log("");
+
+console.log("t=60: Spot = 1.1060 (index 6)");
+console.log(" Rehedge (delta change): Assume +10M");
+console.log(" Cumulative hedge: -50M + 10M = -40M");
+console.log(" Update weighted avg spot");
+console.log(" Assume weighted avg = 1.1015 (simplified)");
+console.log("");
+
+const cumulativeHedge = -40; // millions
+const avgSpot = 1.1015;
+const spotAt60 = spots[6]; // 1.1060
+const spotAt70 = spots[7]; // 1.1050
+
+console.log("Position after t=60 hedge:");
+console.log(" Cumulative hedge: " + cumulativeHedge + "M");
+console.log(" Weighted avg spot: " + avgSpot);
+console.log("");
+
+console.log("=== BEFORE FIX (WRONG) ===");
+console.log("t=70: Spot = " + spotAt70);
+console.log(" Hedge P&L frozen at t=60 spot level");
+console.log(" Hedge P&L = " + cumulativeHedge + " × (" + spotAt60 + " - " + avgSpot + ")");
+const wrongPnL = cumulativeHedge * (spotAt60 - avgSpot);
+console.log(" Hedge P&L = " + wrongPnL + "M = " + (wrongPnL * 1000) + "k");
+console.log(" Problem: Uses spot from t=60 (1.1060), not current spot at t=70 (1.1050)");
+console.log("");
+
+console.log("=== AFTER FIX (CORRECT) ===");
+console.log("t=70: Spot = " + spotAt70);
+console.log(" Hedge P&L mark-to-market at current spot");
+console.log(" Hedge P&L = " + cumulativeHedge + " × (" + spotAt70 + " - " + avgSpot + ")");
+const correctPnL = cumulativeHedge * (spotAt70 - avgSpot);
+console.log(" Hedge P&L = " + correctPnL + "M = " + (correctPnL * 1000) + "k");
+console.log(" Correct: Uses current spot at t=70 (1.1050)");
+console.log("");
+
+console.log("Difference:");
+const difference = correctPnL - wrongPnL;
+console.log(" " + (difference * 1000).toFixed(1) + "k");
+console.log("");
+
+console.log("Explanation:");
+console.log(" Cumulative hedge: -40M (short position)");
+console.log(" Weighted avg: 1.1015");
+console.log(" Spot at t=60: 1.1060 (higher)");
+console.log(" Spot at t=70: 1.1050 (moved down from t=60)");
+console.log("");
+console.log(" Since we're short and spot moved DOWN from 1.1060 to 1.1050:");
+console.log(" → We make money! Hedge P&L should IMPROVE");
+console.log("");
+console.log(" Wrong (frozen at t=60):");
+console.log(" -40M × (1.1060 - 1.1015) = -40M × 0.0045 = -1.8M = -1800k (loss)");
+console.log("");
+console.log(" Correct (mark-to-market at t=70):");
+console.log(" -40M × (1.1050 - 1.1015) = -40M × 0.0035 = -1.4M = -1400k (smaller loss)");
+console.log("");
+console.log(" Improvement: +400k (because spot moved in our favor)");
+console.log("");
+
+console.log("KEY INSIGHT:");
+console.log(" Hedge P&L MUST always be calculated as:");
+console.log(" Hedge_P&L = Cumulative_Hedge × (Current_Spot - Weighted_Avg_Spot)");
+console.log("");
+console.log(" Where 'Current_Spot' is the spot at the TIME we're displaying,");
+console.log(" NOT the spot at the last hedge time.");
+console.log("");
+console.log(" This is mark-to-market: the hedge position gets revalued");
+console.log(" at every time step, even when we're not actively trading.");
+
+console.log("\n=== Test Complete ===");
diff --git a/test_realized_vol.js b/test_realized_vol.js
new file mode 100644
index 000000000..77286af96
--- /dev/null
+++ b/test_realized_vol.js
@@ -0,0 +1,64 @@
+// Test realized volatility calculation
+
+function calculateRealizedVolatility(spotPrices) {
+ // Calculate log returns
+ const returns = [];
+ for (let i = 1; i < spotPrices.length; i++) {
+ returns.push(Math.log(spotPrices[i] / spotPrices[i-1]));
+ }
+
+ // Calculate 24-hour variance (sum of squared returns)
+ const variance24h = returns.reduce((sum, r) => sum + r*r, 0);
+
+ // Annualize
+ const timeCoveredDays = 1.0;
+ const annualizedVariance = variance24h * (365 / timeCoveredDays);
+
+ // Get volatility
+ const realizedVol = Math.sqrt(annualizedVariance);
+
+ return realizedVol;
+}
+
+// Test 1: Flat spot series (no volatility)
+console.log("Test 1: Flat spot series");
+const flatSpots = new Array(145).fill(1.1000);
+const flatVol = calculateRealizedVolatility(flatSpots);
+console.log("Expected: 0%, Actual:", (flatVol * 100).toFixed(2) + "%");
+console.log("Pass:", flatVol === 0 ? "✓" : "✗");
+
+// Test 2: Known volatility
+console.log("\nTest 2: Simulated spot series with known moves");
+const spots = [1.1000];
+// Create 144 small moves of 0.001 each (upward trend)
+for (let i = 1; i < 145; i++) {
+ spots.push(spots[i-1] * 1.001); // 0.1% move per interval
+}
+
+const testVol = calculateRealizedVolatility(spots);
+console.log("First spot:", spots[0].toFixed(4));
+console.log("Last spot:", spots[144].toFixed(4));
+console.log("Realized vol:", (testVol * 100).toFixed(2) + "%");
+
+// Calculate expected: 144 returns of ln(1.001) each
+const expectedReturn = Math.log(1.001);
+const expectedVariance24h = 144 * expectedReturn * expectedReturn;
+const expectedAnnualVol = Math.sqrt(expectedVariance24h * 365);
+console.log("Expected vol:", (expectedAnnualVol * 100).toFixed(2) + "%");
+console.log("Match:", Math.abs(testVol - expectedAnnualVol) < 0.0001 ? "✓" : "✗");
+
+// Test 3: Random walk
+console.log("\nTest 3: Random walk");
+const randomSpots = [1.1000];
+for (let i = 1; i < 145; i++) {
+ const move = (Math.random() - 0.5) * 0.002;
+ randomSpots.push(randomSpots[i-1] * (1 + move));
+}
+
+const randomVol = calculateRealizedVolatility(randomSpots);
+console.log("First spot:", randomSpots[0].toFixed(4));
+console.log("Last spot:", randomSpots[144].toFixed(4));
+console.log("Realized vol:", (randomVol * 100).toFixed(2) + "%");
+console.log("Reasonable (0.1% - 50%):", randomVol > 0.001 && randomVol < 0.5 ? "✓" : "✗");
+
+console.log("\n=== All tests complete ===");
diff --git a/test_variable_frequencies.js b/test_variable_frequencies.js
new file mode 100644
index 000000000..8a9349b71
--- /dev/null
+++ b/test_variable_frequencies.js
@@ -0,0 +1,135 @@
+// Test variable base data frequency and hedging frequency
+
+function calculateHedgeTimes(totalMinutes, hedgeFrequency) {
+ const hedgeTimes = [];
+ let hedgeTime = 0;
+ while (hedgeTime <= totalMinutes) {
+ hedgeTimes.push(hedgeTime);
+ hedgeTime += hedgeFrequency;
+ }
+ return hedgeTimes;
+}
+
+console.log("=== Test 1: Base=10, Hedge=10, 145 points (original config) ===");
+{
+ const baseFreq = 10;
+ const hedgeFreq = 10;
+ const numPoints = 145;
+ const totalMinutes = (numPoints - 1) * baseFreq;
+ const hedgeTimes = calculateHedgeTimes(totalMinutes, hedgeFreq);
+
+ console.log("Base frequency:", baseFreq, "min");
+ console.log("Hedge frequency:", hedgeFreq, "min");
+ console.log("Number of data points:", numPoints);
+ console.log("Total time:", totalMinutes, "minutes =", (totalMinutes / 60), "hours");
+ console.log("Hedge times:", hedgeTimes.length, "points");
+ console.log("Expected hedge times: 145 (every data point)");
+ console.log("Match:", hedgeTimes.length === 145 ? "✓" : "✗");
+ console.log("Last hedge time:", hedgeTimes[hedgeTimes.length - 1]);
+ console.log("Aligned (last hedge = total):", hedgeTimes[hedgeTimes.length - 1] === totalMinutes ? "✓" : "✗");
+}
+
+console.log("\n=== Test 2: Base=10, Hedge=60, 145 points ===");
+{
+ const baseFreq = 10;
+ const hedgeFreq = 60;
+ const numPoints = 145;
+ const totalMinutes = (numPoints - 1) * baseFreq;
+ const hedgeTimes = calculateHedgeTimes(totalMinutes, hedgeFreq);
+
+ console.log("Base frequency:", baseFreq, "min");
+ console.log("Hedge frequency:", hedgeFreq, "min");
+ console.log("Number of data points:", numPoints);
+ console.log("Total time:", totalMinutes, "minutes =", (totalMinutes / 60), "hours");
+ console.log("Hedge times:", hedgeTimes.length, "points");
+ console.log("Expected: [0, 60, 120, ..., 1440] → 25 points");
+ console.log("Match:", hedgeTimes.length === 25 ? "✓" : "✗");
+ console.log("First few hedge times:", hedgeTimes.slice(0, 5));
+ console.log("Last hedge time:", hedgeTimes[hedgeTimes.length - 1]);
+ console.log("Aligned (last hedge = total):", hedgeTimes[hedgeTimes.length - 1] === totalMinutes ? "✓" : "✗");
+
+ // Check spot indices
+ console.log("\nSpot indices at hedge times:");
+ console.log(" Hedge@0 → spot[0]");
+ console.log(" Hedge@60 → spot[" + (60 / baseFreq) + "]");
+ console.log(" Hedge@120 → spot[" + (120 / baseFreq) + "]");
+ console.log(" Hedge@1440 → spot[" + (1440 / baseFreq) + "]");
+}
+
+console.log("\n=== Test 3: Base=10, Hedge=60, 8 points (70 min total - MISALIGNED) ===");
+{
+ const baseFreq = 10;
+ const hedgeFreq = 60;
+ const numPoints = 8;
+ const totalMinutes = (numPoints - 1) * baseFreq;
+ const hedgeTimes = calculateHedgeTimes(totalMinutes, hedgeFreq);
+
+ console.log("Base frequency:", baseFreq, "min");
+ console.log("Hedge frequency:", hedgeFreq, "min");
+ console.log("Number of data points:", numPoints);
+ console.log("Total time:", totalMinutes, "minutes");
+ console.log("Hedge times:", hedgeTimes);
+ console.log("Number of hedge times:", hedgeTimes.length);
+ console.log("Expected: [0, 60] → 2 points");
+ console.log("Match:", hedgeTimes.length === 2 ? "✓" : "✗");
+ console.log("Last hedge time:", hedgeTimes[hedgeTimes.length - 1]);
+ console.log("Total time:", totalMinutes);
+ console.log("Misaligned (last hedge < total):", hedgeTimes[hedgeTimes.length - 1] < totalMinutes ? "✓" : "✗");
+ console.log("→ Need to add final row at time", totalMinutes);
+
+ // Check for realized vol calculation
+ console.log("\nRealized vol should use:");
+ console.log(" Hedge times: [0, 60]");
+ console.log(" Plus final time: 70");
+ console.log(" Spot indices: [0, 6, 7]");
+ console.log(" Returns: ln(spot[6]/spot[0]), ln(spot[7]/spot[6])");
+}
+
+console.log("\n=== Test 4: Base=30, Hedge=120, 97 points (48 hours) ===");
+{
+ const baseFreq = 30;
+ const hedgeFreq = 120;
+ const numPoints = 97;
+ const totalMinutes = (numPoints - 1) * baseFreq;
+ const totalHours = totalMinutes / 60;
+ const hedgeTimes = calculateHedgeTimes(totalMinutes, hedgeFreq);
+
+ console.log("Base frequency:", baseFreq, "min");
+ console.log("Hedge frequency:", hedgeFreq, "min");
+ console.log("Number of data points:", numPoints);
+ console.log("Total time:", totalMinutes, "minutes =", totalHours, "hours");
+ console.log("Hedge times:", hedgeTimes.length, "points");
+ console.log("Expected: [0, 120, 240, ..., 2880] → 25 points");
+ console.log("Match:", hedgeTimes.length === 25 ? "✓" : "✗");
+ console.log("First few hedge times:", hedgeTimes.slice(0, 5));
+ console.log("Last hedge time:", hedgeTimes[hedgeTimes.length - 1]);
+ console.log("Aligned (last hedge = total):", hedgeTimes[hedgeTimes.length - 1] === totalMinutes ? "✓" : "✗");
+
+ // Check spot indices
+ console.log("\nSpot indices at hedge times:");
+ console.log(" Hedge@0 → spot[0]");
+ console.log(" Hedge@120 → spot[" + (120 / baseFreq) + "]");
+ console.log(" Hedge@240 → spot[" + (240 / baseFreq) + "]");
+}
+
+console.log("\n=== Validation Tests ===");
+{
+ // Test validation: hedge frequency >= base frequency
+ const validCombos = [
+ {base: 10, hedge: 10, valid: true},
+ {base: 10, hedge: 60, valid: true},
+ {base: 30, hedge: 30, valid: true},
+ {base: 30, hedge: 120, valid: true},
+ {base: 60, hedge: 10, valid: false}, // Invalid
+ {base: 120, hedge: 60, valid: false}, // Invalid
+ ];
+
+ console.log("\nTesting validation rule: hedge_freq >= base_freq");
+ validCombos.forEach(({base, hedge, valid}) => {
+ const isValid = hedge >= base;
+ const status = isValid === valid ? "✓" : "✗";
+ console.log(` Base=${base}, Hedge=${hedge}: Expected ${valid}, Got ${isValid} ${status}`);
+ });
+}
+
+console.log("\n=== All Tests Complete ===");
diff --git a/test_variable_length.js b/test_variable_length.js
new file mode 100644
index 000000000..fb2c531b3
--- /dev/null
+++ b/test_variable_length.js
@@ -0,0 +1,104 @@
+// Test variable-length spot series calculations
+
+const INTERVAL_MINUTES = 10;
+
+function calculateTimePeriod(numPoints) {
+ const numIntervals = numPoints - 1;
+ const totalMinutes = numIntervals * INTERVAL_MINUTES;
+ const totalHours = totalMinutes / 60;
+ const totalDays = totalHours / 24;
+ const timeYears = totalDays / 365;
+
+ return { numIntervals, totalMinutes, totalHours, totalDays, timeYears };
+}
+
+console.log("=== Variable-Length Time Period Calculations ===\n");
+
+// Test Case 1: 1 hour of data (7 points)
+console.log("Test Case 1: 1 hour of data");
+const test1 = calculateTimePeriod(7);
+console.log(" Points: 7");
+console.log(" Intervals:", test1.numIntervals);
+console.log(" Total minutes:", test1.totalMinutes);
+console.log(" Total hours:", test1.totalHours);
+console.log(" Total days:", test1.totalDays);
+console.log(" Time (years):", test1.timeYears.toFixed(10));
+console.log(" Expected: 1/(365×24) =", (1/(365*24)).toFixed(10));
+console.log(" Match:", Math.abs(test1.timeYears - 1/(365*24)) < 0.0000001 ? "✓" : "✗");
+
+// Test Case 2: 24 hours (145 points) - original
+console.log("\nTest Case 2: 24 hours of data (original)");
+const test2 = calculateTimePeriod(145);
+console.log(" Points: 145");
+console.log(" Intervals:", test2.numIntervals);
+console.log(" Total minutes:", test2.totalMinutes);
+console.log(" Total hours:", test2.totalHours);
+console.log(" Total days:", test2.totalDays);
+console.log(" Time (years):", test2.timeYears.toFixed(10));
+console.log(" Expected: 1/365 =", (1/365).toFixed(10));
+console.log(" Match:", Math.abs(test2.timeYears - 1/365) < 0.0000001 ? "✓" : "✗");
+
+// Test Case 3: 48 hours (289 points)
+console.log("\nTest Case 3: 48 hours of data");
+const test3 = calculateTimePeriod(289);
+console.log(" Points: 289");
+console.log(" Intervals:", test3.numIntervals);
+console.log(" Total minutes:", test3.totalMinutes);
+console.log(" Total hours:", test3.totalHours);
+console.log(" Total days:", test3.totalDays);
+console.log(" Time (years):", test3.timeYears.toFixed(10));
+console.log(" Expected: 2/365 =", (2/365).toFixed(10));
+console.log(" Match:", Math.abs(test3.timeYears - 2/365) < 0.0000001 ? "✓" : "✗");
+
+// Test Case 4: 6 hours (37 points)
+console.log("\nTest Case 4: 6 hours of data");
+const test4 = calculateTimePeriod(37);
+console.log(" Points: 37");
+console.log(" Intervals:", test4.numIntervals);
+console.log(" Total minutes:", test4.totalMinutes);
+console.log(" Total hours:", test4.totalHours);
+console.log(" Total days:", test4.totalDays);
+console.log(" Time (years):", test4.timeYears.toFixed(10));
+console.log(" Expected: 0.25/365 =", (0.25/365).toFixed(10));
+console.log(" Match:", Math.abs(test4.timeYears - 0.25/365) < 0.0000001 ? "✓" : "✗");
+
+// Test realized volatility annualization
+console.log("\n=== Realized Volatility Annualization ===\n");
+
+function calculateRealizedVolatility(spotPrices, totalDays) {
+ const returns = [];
+ for (let i = 1; i < spotPrices.length; i++) {
+ returns.push(Math.log(spotPrices[i] / spotPrices[i-1]));
+ }
+
+ const varianceTotalPeriod = returns.reduce((sum, r) => sum + r*r, 0);
+ const annualizedVariance = varianceTotalPeriod * (365 / totalDays);
+ const realizedVol = Math.sqrt(annualizedVariance);
+
+ return realizedVol;
+}
+
+// Test with 1 hour vs 24 hours - same return pattern
+console.log("Test: Annualization consistency");
+const constantReturn = 0.001; // 0.1% per interval
+
+// 1 hour (7 points = 6 intervals)
+const spots1h = [1.1000];
+for (let i = 0; i < 6; i++) {
+ spots1h.push(spots1h[spots1h.length - 1] * Math.exp(constantReturn));
+}
+const vol1h = calculateRealizedVolatility(spots1h, test1.totalDays);
+
+// 24 hours (145 points = 144 intervals)
+const spots24h = [1.1000];
+for (let i = 0; i < 144; i++) {
+ spots24h.push(spots24h[spots24h.length - 1] * Math.exp(constantReturn));
+}
+const vol24h = calculateRealizedVolatility(spots24h, test2.totalDays);
+
+console.log(" 1-hour vol:", (vol1h * 100).toFixed(2) + "%");
+console.log(" 24-hour vol:", (vol24h * 100).toFixed(2) + "%");
+console.log(" Difference:", Math.abs(vol1h - vol24h).toFixed(6));
+console.log(" Similar (within 0.01):", Math.abs(vol1h - vol24h) < 0.01 ? "✓" : "✗");
+
+console.log("\n=== All Tests Complete ===");