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…
+
+
+ + +
Awaiting upload. Columns: timestamp, PAIR1, …
+
+
+ + +
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

+
+
+ + +
Pairs populated after loading both files. Hold Ctrl/Cmd to select several. Leave empty for all.
+
+
+ + +
e.g. ATM, 25D Call, 10D Put. Leave empty to include every strike label.
+
+
+ + +
+
+ +

Variance Decay

+
+ + + +
+ + +
+ + +
+ + + + + + + +
+
+

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…
+
+
+ + +
Awaiting upload. Columns: timestamp, PAIR1, …
+
+
+ + +
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

+
+
+ + +
Pairs populated after loading both files. Hold Ctrl/Cmd to select several. Leave empty for all.
+
+
+ + +
e.g. ATM, 25D Call, 10D Put. Leave empty to include every strike label.
+
+
+ + +
+
+ +

Variance Decay

+
+ + + +
+ + +
+ + +
+ + + + + + 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

+ + +
+ + +
+ + +
+
+ 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 +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ +
+ + +
+
+ +
+ +
+
+ + +
+ + +
+
+
+ +
+
+ +
+ +
+
+ + Custom Strikes: 0/5 +
+
+
+ +
+
+
+
+
+ + + +
+

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%)
  • +
+
+ + + +
+ + +
+ + + + + +
+
Calculating...
+
+
0%
+
+
+
+ + + +
+ + +
+ + + + + + + + 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 + + + +
+ + + 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 ===");