diff --git a/application/runtime_strategy_adapters.py b/application/runtime_strategy_adapters.py index b855b8f..8e43d16 100644 --- a/application/runtime_strategy_adapters.py +++ b/application/runtime_strategy_adapters.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from typing import Any +from quant_platform_kit.common.runtime_inputs import build_semiconductor_rotation_indicators_from_history from quant_platform_kit.strategy_contracts import build_account_state_from_portfolio_snapshot @@ -119,29 +120,11 @@ def build_semiconductor_indicators(self, market_data_source, *, trend_window: in if len(soxx_history) < trend_window: raise RuntimeError(f"SOXX history has {len(soxx_history)} candles; need at least {trend_window}") - soxl_closes = [float(candle["close"]) for candle in soxl_history[-trend_window:]] - soxx_all_closes = [float(candle["close"]) for candle in soxx_history] - soxx_trend_closes = soxx_all_closes[-trend_window:] - soxx_ma20_closes = soxx_all_closes[-20:] - previous_soxx_ma20_closes = soxx_all_closes[-21:-1] - soxx_ma20 = sum(soxx_ma20_closes) / 20 - previous_soxx_ma20 = ( - sum(previous_soxx_ma20_closes) / 20 - if len(previous_soxx_ma20_closes) == 20 - else soxx_ma20 + return build_semiconductor_rotation_indicators_from_history( + soxl_history=(float(candle["close"]) for candle in soxl_history), + soxx_history=(float(candle["close"]) for candle in soxx_history), + trend_ma_window=trend_window, ) - return { - "soxl": { - "price": soxl_closes[-1], - "ma_trend": sum(soxl_closes) / trend_window, - }, - "soxx": { - "price": soxx_all_closes[-1], - "ma_trend": sum(soxx_trend_closes) / trend_window, - "ma20": soxx_ma20, - "ma20_slope": soxx_ma20 - previous_soxx_ma20, - }, - } def build_account_state_from_snapshot(self, snapshot) -> dict[str, object]: return build_account_state_from_portfolio_snapshot( diff --git a/notifications/telegram.py b/notifications/telegram.py index 99e0e23..7adb581 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -50,6 +50,11 @@ "market_status_blend_gate_defensive": "🛡️ 降杠杆({asset})", "signal_blend_gate_risk_on": "{trend_symbol} 站上 {window} 日门槛线,持有 SOXL {soxl_ratio} + SOXX {soxx_ratio}", "signal_blend_gate_defensive": "{trend_symbol} 跌破门槛线,防守持有 SOXX {soxx_ratio}", + "market_status_blend_gate_overlay_capped": "🧯 过热降档({asset})", + "signal_blend_gate_overlay_capped": "{trend_symbol} 仍在 {window} 日门槛线上方,但触发过热降档({reasons}),目标仓位 {allocation_text}", + "blend_gate_reason_rsi_cap": "RSI 超阈值", + "blend_gate_reason_bollinger_cap": "突破布林上轨", + "blend_gate_reason_volatility_delever": "{symbol} {window} 日年化波动率 {volatility} 高于 {threshold},SOXL 转向 {redirect_symbol}", "limit_buy": "限价买入", "market_buy": "市价买入", "market_sell": "市价卖出", @@ -128,6 +133,11 @@ "market_status_blend_gate_defensive": "🛡️ DE-LEVER ({asset})", "signal_blend_gate_risk_on": "{trend_symbol} above {window}d gated entry, hold SOXL {soxl_ratio} + SOXX {soxx_ratio}", "signal_blend_gate_defensive": "{trend_symbol} below gated entry, hold defensive SOXX {soxx_ratio}", + "market_status_blend_gate_overlay_capped": "🧯 HEAT-CAP ({asset})", + "signal_blend_gate_overlay_capped": "{trend_symbol} stays above the {window}d gate, but overlay cap ({reasons}) cuts exposure to {allocation_text}", + "blend_gate_reason_rsi_cap": "RSI over threshold", + "blend_gate_reason_bollinger_cap": "price above upper band", + "blend_gate_reason_volatility_delever": "{symbol} {window}d annualized volatility {volatility} is above {threshold}; redirect SOXL to {redirect_symbol}", "limit_buy": "Limit Buy", "market_buy": "Market Buy", "market_sell": "Market Sell", diff --git a/requirements.txt b/requirements.txt index 98f5bbb..d06d077 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@573fc9e9917cf1f2c1acda9232c5a23a8a05d797 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@53911cbe32f6932e759522e54aa38ca5350aa44e +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@8769362096227320bc05c791b5244d4b3e88db50 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ed55a6af0245323dbed82060e89be96d8f77f756 pandas requests pandas_market_calendars diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 4e022eb..bc73d6c 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -45,6 +45,17 @@ def test_build_translator_supports_chinese(self): ), "SOXX 站上 140 日门槛线,持有 SOXL 70.0% + SOXX 20.0%", ) + self.assertEqual( + translate( + "blend_gate_reason_volatility_delever", + symbol="SOXX", + window=20, + volatility="55.0%", + threshold="50.0%", + redirect_symbol="SOXX", + ), + "SOXX 20 日年化波动率 55.0% 高于 50.0%,SOXL 转向 SOXX", + ) self.assertEqual( translate( "small_account_warning_note", diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 568d48b..78e459b 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -477,6 +477,12 @@ def fake_history(_client, symbol): self.assertAlmostEqual(indicators["soxx"]["ma_trend"], sum(210.0 + idx for idx in range(10, 160)) / 150) self.assertAlmostEqual(indicators["soxx"]["ma20"], sum(210.0 + idx for idx in range(140, 160)) / 20) self.assertGreater(indicators["soxx"]["ma20_slope"], 0.0) + self.assertEqual(indicators["soxx"]["rsi14"], 100.0) + self.assertIn("realized_volatility_20", indicators["soxx"]) + self.assertEqual( + indicators["soxx"]["realized_volatility"], + indicators["soxx"]["realized_volatility_20"], + ) if __name__ == "__main__":