core.test_analysis

Tests for Hedge Analysis Functions

This module tests the pure functions in f.domains.hedge.core.analysis that analyze hedge portfolio performance and extract key metrics.

MATHEMATICAL BACKGROUND

The analysis functions evaluate hedged LP positions by computing:

  1. Scenario Analysis: Finding worst/best/base case outcomes

    • Worst case: argmin(scenarios, key=ROI)
    • Best case: argmax(scenarios, key=ROI)
    • Base case: argmin(|variation - target|)
  2. Break-even Analysis: Finding where ROI approaches zero

    • Break-even: scenario where |ROI| <= tolerance
  3. Capital Breakdown: Decomposing final capital into components

    • net_capital = LP_value + fees - hedge_cost + hedge_payoff
  4. Unhedged Loss: Maximum IL without hedge protection

    • worst_loss = max(|IL_vs_deposit|) for all scenarios
  5. Strike Profit Matrix: Payoff of each strike at each scenario

    • payoff[i,j] = max(K_i - S_j, 0) * quantity_i

KEY FORMULAS

Put Option Payoff: payoff = max(K - S, 0) * quantity

Where: K = strike price S = spot price at expiry quantity = number of contracts

ROI (Return on Investment): ROI = (capital / total_investment) - 1

APR (Annualized Percentage Rate): APR = (1 + ROI)^(365/days) - 1

Impermanent Loss vs Deposit: IL_deposit = (V_LP / I_initial) - 1

STANDARD TEST PARAMETERS

  • P0 (spot price): 3050 USD
  • P_min: 2200 USD
  • Investment: 100,000 USD
  • Fee yield: 63% APR
  • Volatility: 70%
  • Precision: 50 decimal places

Functions Tested (10 functions, 30 tests total)

  1. find_worst_case_scenario - 3 tests
  2. find_best_case_scenario - 3 tests
  3. find_base_case_scenario - 3 tests
  4. find_break_even_scenario - 3 tests
  5. calculate_unhedged_worst_loss - 3 tests
  6. build_key_takeaways - 3 tests
  7. calculate_capital_breakdown_entry - 3 tests
  8. build_capital_breakdown - 3 tests
  9. calculate_strike_payoff_at_price - 3 tests
  10. build_strike_profit_matrix - 3 tests
@pytest.fixture
def sample_scenarios():

Sample scenario results for testing.

Four scenarios representing price movements from -30% to +20%:

Variation Price LP Value Capital ROI APR
-30% 2135 75,000 95,000 -5% -15%
-10% 2745 92,000 105,000 +5% +18%
0% 3050 100,000 110,000 +10% +35%
+20% 3660 110,000 125,000 +25% +75%
@pytest.fixture
def sample_scenario_points():

Sample IL scenario points for testing.

Three scenarios showing impermanent loss at different price levels:

Variation Price LP Value HODL Value IL vs HODL IL vs Deposit
-30% 2135 75,000 80,000 -6.25% -25%
0% 3050 100,000 100,000 0% 0%
+20% 3660 105,000 110,000 -4.5% +5%

Note: IL vs HODL = (V_LP / V_HODL) - 1 IL vs Deposit = (V_LP / I_initial) - 1 where I_initial = 100,000

@pytest.fixture
def sample_positions():

Sample hedge positions for testing.

Two put options for downside protection:

Strike Quantity Price (ratio) Premium
2600 5 0.05 (5%) $762.50
2200 10 0.03 (3%) $915.00

Total premium = $1,677.50

def test_find_worst_case_multiple_items(sample_scenarios):

Test: Find scenario with minimum ROI from multiple scenarios.

MATHEMATICAL BACKGROUND

Worst case is defined as the scenario with the lowest ROI:

worst = argmin(scenarios, key=lambda s: s.roi)

This identifies the scenario where the hedged position performs worst, which typically occurs during significant downside moves where hedge protection may be insufficient.

NUMERICAL EXAMPLE

Input scenarios: [-30%: ROI=-5%, -10%: ROI=+5%, 0%: ROI=+10%, +20%: ROI=+25%]

Calculation: min(ROI) = min(-0.05, 0.05, 0.10, 0.25) = -0.05 Corresponding variation = -30%

Expected: worst.roi = -5% worst.variation = -30%

def test_find_worst_case_single_item():

Test: Find worst case when only one scenario exists.

MATHEMATICAL BACKGROUND

With a single scenario, that scenario is by definition both the worst and best case.

NUMERICAL EXAMPLE

Input: Single scenario: variation=0%, ROI=+10%

Expected: worst.roi = +10% (only option)

def test_find_worst_case_empty_list():

Test: Handle empty scenario list gracefully.

EDGE CASE

When no scenarios are provided, return None rather than raising an exception. This allows callers to handle the case explicitly.

Expected: worst = None

def test_find_best_case_multiple_items(sample_scenarios):

Test: Find scenario with maximum ROI from multiple scenarios.

MATHEMATICAL BACKGROUND

Best case is defined as the scenario with the highest ROI:

best = argmax(scenarios, key=lambda s: s.roi)

This identifies the scenario where the hedged position performs best, typically when price moves favorably and hedge costs are minimized.

NUMERICAL EXAMPLE

Input scenarios: [-30%: ROI=-5%, -10%: ROI=+5%, 0%: ROI=+10%, +20%: ROI=+25%]

Calculation: max(ROI) = max(-0.05, 0.05, 0.10, 0.25) = 0.25 Corresponding variation = +20%

Expected: best.roi = +25% best.variation = +20%

def test_find_best_case_single_item():

Test: Find best case when only one scenario exists.

NUMERICAL EXAMPLE

Input: Single scenario: variation=-10%, ROI=0%

Expected: best.roi = 0% (only option)

def test_find_best_case_empty_list():

Test: Handle empty scenario list gracefully.

Expected: best = None

def test_find_base_case_exact_match(sample_scenarios):

Test: Find base case when exact variation=0 exists.

MATHEMATICAL BACKGROUND

Base case is the scenario closest to a target variation (default 0%):

base = argmin(scenarios, key=lambda s: |s.variation - target|)

When target=0%, this represents the "no price change" scenario, useful as a reference point for comparing other outcomes.

NUMERICAL EXAMPLE

Input scenarios: variations = [-30%, -10%, 0%, +20%] target = 0%

Calculation: distances = [|-0.30 - 0| = 0.30, |-0.10 - 0| = 0.10, |0 - 0| = 0, |0.20 - 0| = 0.20] min_distance = 0 at variation = 0%

Expected: base.variation = 0%

def test_find_base_case_nearest_match():

Test: Find base case when exact match doesn't exist.

NUMERICAL EXAMPLE

Input scenarios: variations = [-15%, +5%] target = 0%

Calculation: distances = [|-0.15 - 0| = 0.15, |0.05 - 0| = 0.05] min_distance = 0.05 at variation = +5%

Note: +5% is closer to 0% than -15%

Expected: base.variation = +5%

def test_find_base_case_empty_list():

Test: Handle empty scenario list gracefully.

Expected: base = None

def test_find_break_even_exact_zero():

Test: Find break-even when ROI is exactly zero.

MATHEMATICAL BACKGROUND

Break-even is the scenario where ROI is within tolerance of zero:

break_even = scenario where |ROI| <= epsilon

This represents the price point where the strategy neither makes nor loses money, useful for risk assessment.

NUMERICAL EXAMPLE

Input: scenario: variation=-8%, ROI=0% tolerance = 1%

Calculation: |0| = 0 <= 0.01 (tolerance) Found break-even

Expected: break_even.roi = 0%

def test_find_break_even_near_zero():

Test: Find break-even when ROI is near zero (within tolerance).

NUMERICAL EXAMPLE

Input scenarios: 1. variation=-10%, ROI=+0.5% 2. variation=-20%, ROI=-5% tolerance = 1%

Calculation: |0.005| = 0.005 <= 0.01 (within tolerance) - MATCH |-0.05| = 0.05 > 0.01 (outside tolerance)

Expected: break_even.variation = -10%

def test_find_break_even_none_found():

Test: Return None when no break-even point within tolerance.

NUMERICAL EXAMPLE

Input scenarios: 1. variation=-30%, ROI=-5% 2. variation=+20%, ROI=+25% tolerance = 1%

Calculation: |-0.05| = 0.05 > 0.01 (outside tolerance) |0.25| = 0.25 > 0.01 (outside tolerance)

Expected: break_even = None (no scenario within tolerance)

def test_unhedged_worst_loss_negative_il(sample_scenario_points):

Test: Calculate maximum IL from scenarios with negative IL.

MATHEMATICAL BACKGROUND

Unhedged worst loss is the maximum absolute impermanent loss:

worst_loss = max(|IL_vs_deposit|) for all scenarios

IL is typically negative (a loss), so we take absolute value. This represents the worst-case loss without any hedge protection.

NUMERICAL EXAMPLE

Input scenario points: IL_vs_deposit = [-25%, 0%, +5%]

Calculation: absolute_values = [|-0.25|, |0|, |0.05|] = [0.25, 0, 0.05] max_loss = max(0.25, 0, 0.05) = 0.25

Expected: worst_loss = 25%

def test_unhedged_worst_loss_zero_il():

Test: Calculate worst loss when IL is zero (no price change).

NUMERICAL EXAMPLE

Input: Single point: IL_vs_deposit = 0%

Calculation: |0| = 0

Expected: worst_loss = 0%

def test_unhedged_worst_loss_empty_list():

Test: Handle empty scenario point list gracefully.

EDGE CASE

With no scenarios, return 0 as there's no loss to measure.

Expected: worst_loss = 0

def test_build_key_takeaways_standard(sample_scenarios, sample_scenario_points):

Test: Build comprehensive key takeaways from scenarios.

MATHEMATICAL BACKGROUND

Key takeaways aggregate critical metrics from the analysis:

KeyTakeaways = {
    worst_case: scenario with min(ROI),
    best_case: scenario with max(ROI),
    base_case: scenario closest to variation=0,
    break_even_price: price where ROI ~ 0,
    break_even_percent: variation at break-even,
    max_protected_loss: worst_case.ROI,
    unhedged_loss: max(|IL_vs_deposit|)
}

NUMERICAL EXAMPLE

Input: scenarios: 4 scenarios from -30% to +20% ROIs: [-5%, +5%, +10%, +25%] IL_vs_deposit: [-25%, 0%, +5%]

Calculation: worst_case.roi = min(-0.05, 0.05, 0.10, 0.25) = -0.05 best_case.roi = max(-0.05, 0.05, 0.10, 0.25) = 0.25 base_case.variation = 0 (exact match) unhedged_loss = max(|-0.25|, |0|, |0.05|) = 0.25

Expected: worst_case.roi = -5% best_case.roi = +25% base_case.variation = 0% unhedged_loss = 25%

def test_build_key_takeaways_edge_cases():

Test: Build key takeaways when all values are zero.

NUMERICAL EXAMPLE

Input: Single scenario: variation=0%, ROI=0% Single point: IL=0%

Expected: All metrics at zero (no gain, no loss)

def test_build_key_takeaways_empty_scenarios():

Test: Handle empty input gracefully.

EDGE CASE

With no scenarios, return None rather than incomplete data.

Expected: takeaways = None

def test_capital_breakdown_entry_positive_payoff(sample_scenarios, sample_positions):

Test: Calculate capital breakdown when hedge has positive payoff (ITM).

MATHEMATICAL BACKGROUND

Capital breakdown decomposes the final capital into components:

net_capital = LP_value + fees - hedge_cost + hedge_payoff

Where: LP_value: Value of LP position at scenario price fees: Earned fees over the period hedge_cost: Premium paid for hedge (negative) hedge_payoff: max(K - S, 0) * quantity for each position

NUMERICAL EXAMPLE

Input: scenario: variation=-30%, price=2135 positions: Put 2600 x 5, Put 2200 x 10 fees = $18,295 total_cost = $1,677.50 spot_price = 3050

Calculation: future_spot = 3050 * (1 + (-0.30)) = 3050 * 0.70 = 2135 Put 2600 payoff = max(2600 - 2135, 0) * 5 = 465 * 5 = 2325 Put 2200 payoff = max(2200 - 2135, 0) * 10 = 65 * 10 = 650 Total hedge_payoff = 2325 + 650 = 2975

Expected: entry.variation = -30% entry.lp_position = 75,000 entry.fees_earned = 18,295 entry.hedge_cost = -1,677.50 (negative) entry.hedge_payoff > 0 (ITM puts)

def test_capital_breakdown_entry_zero_payoff(sample_scenarios, sample_positions):

Test: Calculate capital breakdown when hedge has zero payoff (OTM).

NUMERICAL EXAMPLE

Input: scenario: variation=+20%, price=3660 positions: Put 2600 x 5, Put 2200 x 10 spot_price = 3050

Calculation: future_spot = 3050 * (1 + 0.20) = 3050 * 1.20 = 3660 Put 2600 payoff = max(2600 - 3660, 0) * 5 = 0 (OTM) Put 2200 payoff = max(2200 - 3660, 0) * 10 = 0 (OTM) Total hedge_payoff = 0

Note: Both puts are OTM when price rises to 3660

Expected: entry.hedge_payoff = 0

def test_capital_breakdown_entry_negative_variation(sample_scenarios, sample_positions):

Test: Capital breakdown with moderate downside (partial OTM).

NUMERICAL EXAMPLE

Input: scenario: variation=-10%, price=2745 positions: Put 2600 x 5, Put 2200 x 10 spot_price = 3050

Calculation: future_spot = 3050 * (1 + (-0.10)) = 3050 * 0.90 = 2745 Put 2600 payoff = max(2600 - 2745, 0) * 5 = 0 (OTM, 2745 > 2600) Put 2200 payoff = max(2200 - 2745, 0) * 10 = 0 (OTM, 2745 > 2200) Total hedge_payoff = 0

Note: Even at -10%, price (2745) is still above both strikes

Expected: entry.hedge_payoff = 0

def test_build_capital_breakdown_multiple(sample_scenarios, sample_positions):

Test: Build capital breakdown for multiple scenarios.

MATHEMATICAL BACKGROUND

Builds a list of CapitalBreakdownEntry, one for each scenario:

breakdowns = [
    calculate_capital_breakdown_entry(s, positions, fees, cost, spot)
    for s in scenarios
]

NUMERICAL EXAMPLE

Input: 4 scenarios: variations = [-30%, -10%, 0%, +20%]

Expected: 4 breakdown entries, one per scenario

def test_build_capital_breakdown_single():

Test: Build capital breakdown for single scenario.

NUMERICAL EXAMPLE

Input: 1 scenario: variation=0% No hedge positions

Expected: 1 breakdown entry

def test_build_capital_breakdown_empty():

Test: Build capital breakdown for empty scenarios.

Expected: Empty list

def test_strike_payoff_itm():

Test: Calculate put payoff when option is ITM (In-The-Money).

MATHEMATICAL BACKGROUND

Put option payoff at expiry:

payoff = max(K - S, 0) * quantity

Where: K = strike price S = spot price at expiry quantity = number of contracts

ITM condition: S < K (spot below strike)

NUMERICAL EXAMPLE

Input: strike = 2500 quantity = 10 spot_price = 2200

Calculation: intrinsic = max(2500 - 2200, 0) = max(300, 0) = 300 payoff = 300 * 10 = 3000

Expected: payoff = $3,000

def test_strike_payoff_otm():

Test: Calculate put payoff when option is OTM (Out-of-The-Money).

MATHEMATICAL BACKGROUND

OTM condition: S > K (spot above strike) When OTM, intrinsic value is zero.

NUMERICAL EXAMPLE

Input: strike = 2500 quantity = 10 spot_price = 3000

Calculation: intrinsic = max(2500 - 3000, 0) = max(-500, 0) = 0 payoff = 0 * 10 = 0

Expected: payoff = $0

def test_strike_payoff_atm():

Test: Calculate put payoff when option is ATM (At-The-Money).

MATHEMATICAL BACKGROUND

ATM condition: S = K (spot equals strike) At expiry, ATM options have zero intrinsic value.

NUMERICAL EXAMPLE

Input: strike = 2500 quantity = 10 spot_price = 2500

Calculation: intrinsic = max(2500 - 2500, 0) = max(0, 0) = 0 payoff = 0 * 10 = 0

Expected: payoff = $0

def test_strike_profit_matrix_with_selected(sample_positions, sample_scenarios):

Test: Build strike profit matrix with explicitly selected strikes.

MATHEMATICAL BACKGROUND

The strike profit matrix shows each strike's payoff at each scenario:

Matrix structure:
- Rows: strikes (sorted descending)
- Columns: scenarios (variations)
- Values: payoff = max(K - S, 0) * quantity

StrikeProfitEntry = { strike: K, quantity: q, payoffs: [ {variation: v, spot_price: S, payoff: max(K-S,0)*q} for each scenario ] }

NUMERICAL EXAMPLE

Input: positions: Put 2600 x 5, Put 2200 x 10 scenarios: 4 scenarios selected_strikes: [2600, 2200]

Expected: 2 entries (one per strike) 4 payoffs per entry (one per scenario) Strikes sorted descending: [2600, 2200]

def test_strike_profit_matrix_without_selected(sample_positions, sample_scenarios):

Test: Build strike profit matrix auto-extracting strikes from positions.

NUMERICAL EXAMPLE

Input: positions: Put 2600 x 5, Put 2200 x 10 selected_strikes: None (auto-extract)

Expected: 2 entries (one per position with non-zero quantity)

def test_strike_profit_matrix_empty_positions(sample_scenarios):

Test: Build strike profit matrix with no positions.

EDGE CASE

With no positions, return empty matrix.

Expected: matrix = []