Skip to content

Commit 8769362

Browse files
authored
Add SOXX realized volatility indicator (#35)
1 parent 573fc9e commit 8769362

3 files changed

Lines changed: 36 additions & 0 deletions

File tree

src/quant_platform_kit/common/runtime_inputs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ def _std(values: Iterable[float]) -> float:
5151
return float(sqrt(variance))
5252

5353

54+
def _sample_std(values: Iterable[float]) -> float:
55+
values = tuple(values)
56+
if len(values) < 2:
57+
raise ValueError("sample std requires at least two values")
58+
mean_value = _mean(values)
59+
variance = sum((value - mean_value) ** 2 for value in values) / (len(values) - 1)
60+
return float(sqrt(variance))
61+
62+
5463
def _tail_mean(values: tuple[float, ...], window: int) -> float:
5564
if len(values) < window:
5665
raise ValueError("insufficient history for rolling mean")
@@ -63,6 +72,18 @@ def _tail_std(values: tuple[float, ...], window: int) -> float:
6372
return _std(values[-window:])
6473

6574

75+
def _tail_realized_volatility(values: tuple[float, ...], window: int) -> float:
76+
if len(values) < window + 1:
77+
raise ValueError("insufficient history for realized volatility")
78+
tail_values = values[-(window + 1):]
79+
returns: list[float] = []
80+
for previous, current in zip(tail_values, tail_values[1:]):
81+
if previous == 0.0:
82+
raise ValueError("realized volatility requires non-zero prices")
83+
returns.append((current / previous) - 1.0)
84+
return float(_sample_std(returns) * sqrt(252))
85+
86+
6687
def _compute_rsi(values: tuple[float, ...], *, window: int = 14) -> tuple[float, ...]:
6788
if len(values) < window + 1:
6889
raise ValueError("insufficient history for RSI")
@@ -161,6 +182,7 @@ def build_semiconductor_rotation_indicators_from_history(
161182
)
162183
soxx_bb_mid = _tail_mean(soxx_close, 20)
163184
soxx_bb_std = _tail_std(soxx_close, 20)
185+
soxx_realized_volatility_20 = _tail_realized_volatility(soxx_close, 20)
164186
return {
165187
"soxl": {
166188
"price": float(soxl_close[-1]),
@@ -176,6 +198,8 @@ def build_semiconductor_rotation_indicators_from_history(
176198
"bb_mid": soxx_bb_mid,
177199
"bb_upper": soxx_bb_mid + 2.0 * soxx_bb_std,
178200
"bb_lower": soxx_bb_mid - 2.0 * soxx_bb_std,
201+
"realized_volatility": soxx_realized_volatility_20,
202+
"realized_volatility_20": soxx_realized_volatility_20,
179203
},
180204
}
181205

tests/test_ibkr_runtime_inputs.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ def fake_loader(_ib, symbol, duration="2 Y", bar_size="1 day"):
115115
self.assertGreaterEqual(indicators["soxx"]["rsi14_dynamic_threshold"], 70.0)
116116
self.assertGreater(indicators["soxx"]["bb_upper"], indicators["soxx"]["price"])
117117
self.assertLess(indicators["soxx"]["bb_lower"], indicators["soxx"]["price"])
118+
self.assertIn("realized_volatility_20", indicators["soxx"])
119+
self.assertEqual(
120+
indicators["soxx"]["realized_volatility"],
121+
indicators["soxx"]["realized_volatility_20"],
122+
)
118123

119124
def test_build_semiconductor_rotation_indicators_from_history_is_generic(self) -> None:
120125
indicators = build_semiconductor_rotation_indicators_from_history(
@@ -136,6 +141,7 @@ def test_build_semiconductor_rotation_indicators_from_history_is_generic(self) -
136141
self.assertEqual(indicators["soxx"]["rsi14"], 100.0)
137142
self.assertGreaterEqual(indicators["soxx"]["rsi14_dynamic_threshold"], 70.0)
138143
self.assertGreater(indicators["soxx"]["bb_upper"], indicators["soxx"]["price"])
144+
self.assertIn("realized_volatility_20", indicators["soxx"])
139145
wrapped = build_semiconductor_rotation_inputs_from_history(
140146
soxl_history=[100.0 + idx for idx in range(170)],
141147
soxx_history=[200.0 + idx for idx in range(170)],
@@ -174,6 +180,7 @@ def fake_loader(_ib, symbol, duration="2 Y", bar_size="1 day"):
174180
self.assertEqual(payload["derived_indicators"]["soxx"]["ma20"], 200.0)
175181
self.assertEqual(payload["derived_indicators"]["soxx"]["rsi14"], 50.0)
176182
self.assertEqual(payload["derived_indicators"]["soxx"]["rsi14_dynamic_threshold"], 70.0)
183+
self.assertEqual(payload["derived_indicators"]["soxx"]["realized_volatility_20"], 0.0)
177184

178185
def test_build_semiconductor_rotation_indicators_requires_sufficient_history(self) -> None:
179186
def fake_loader(_ib, symbol, duration="2 Y", bar_size="1 day"):

tests/test_longbridge_market_data.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ def test_calculate_rotation_indicators(self) -> None:
7979
self.assertGreaterEqual(indicators["soxx"]["rsi14_dynamic_threshold"], 70.0)
8080
self.assertGreater(indicators["soxx"]["bb_upper"], indicators["soxx"]["price"])
8181
self.assertLess(indicators["soxx"]["bb_lower"], indicators["soxx"]["price"])
82+
self.assertIn("realized_volatility_20", indicators["soxx"])
83+
self.assertEqual(
84+
indicators["soxx"]["realized_volatility"],
85+
indicators["soxx"]["realized_volatility_20"],
86+
)
8287

8388

8489
if __name__ == "__main__":

0 commit comments

Comments
 (0)