diff --git a/src/features/greeks.py b/src/features/greeks.py index e7cf6c5..ae7adfa 100644 --- a/src/features/greeks.py +++ b/src/features/greeks.py @@ -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)): diff --git a/tests/test_greeks.py b/tests/test_greeks.py index 860460c..42585ce 100644 --- a/tests/test_greeks.py +++ b/tests/test_greeks.py @@ -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(