Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/features/greeks.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,31 +236,33 @@ def estimate_iv(self, symbol: str, days: int = 30) -> float:
else:
base_vol = 0.30 # General stocks

# Add some randomness to simulate market conditions
import time
import random
random.seed(int(time.time()) % 100)
adjustment = random.uniform(0.8, 1.2)

return base_vol * adjustment
# Deterministic estimate: return the per-category base volatility.
# Previously this was multiplied by a wall-clock-seeded random factor
# (random.seed(int(time.time()) % 100)), so the same symbol returned
# different IVs on consecutive calls and no Greek or POP derived from
# it was reproducible.
return base_vol

except Exception:
# Fallback to 30% volatility
return 0.30

def get_iv_for_position(self, position: Position) -> List[float]:
def get_iv_for_position(self, position: Position, symbol: Optional[str] = None) -> List[float]:
"""
Get IV estimates for all legs of a position.

Args:
position: Position to estimate IV for
symbol: Underlying symbol, when known, used to pick the base IV

Returns:
List of IV estimates for each leg
"""
# In a real implementation, this would fetch option chain data
# For now, use the same IV for all legs with slight variations
base_iv = self.estimate_iv("SPY") # Use SPY as base
# In a real implementation, this would fetch option chain data.
# Use the underlying symbol when the caller knows it. Without it, assume
# a general single-stock volatility rather than SPY (a low-vol ETF),
# which previously underestimated IV for every individual name.
base_iv = self.estimate_iv(symbol) if symbol else self.estimate_iv("UNKNOWN")

iv_estimates = []
for i, (strike, option_type) in enumerate(zip(position.strikes, position.option_types)):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_greeks.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ def test_iv_estimation(self):
self.assertGreater(iv, 0.05)
self.assertLess(iv, 1.0)

def test_iv_estimation_is_deterministic(self):
"""estimate_iv must return the same value on repeated calls (issue #9)."""
first = self.iv_estimator.estimate_iv('AAPL')
second = self.iv_estimator.estimate_iv('AAPL')
self.assertEqual(first, second)
# And distinct symbol categories get distinct, stable bases.
self.assertEqual(self.iv_estimator.estimate_iv('SPY'), 0.15)
self.assertEqual(self.iv_estimator.estimate_iv('AAPL'), 0.25)
self.assertEqual(self.iv_estimator.estimate_iv('XYZ'), 0.30)

def test_position_iv_estimation(self):
"""Test position-level IV estimation."""
position = Position(
Expand Down