Skip to content

Commit dfd2c3a

Browse files
authored
Wire IBIT zscore exit runtime settings (#119)
* Wire IBIT zscore exit runtime settings * Pin strategies for IBIT zscore runtime support
1 parent f1d9bde commit dfd2c3a

8 files changed

Lines changed: 145 additions & 1 deletion

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ NOTIFY_LANG=en
2323
TELEGRAM_TOKEN=
2424
GLOBAL_TELEGRAM_CHAT_ID=
2525
FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON=
26+
DCA_MODE=
27+
DCA_BASE_INVESTMENT_USD=
28+
IBIT_ZSCORE_EXIT_ENABLED=
29+
IBIT_ZSCORE_EXIT_MODE=
30+
IBIT_ZSCORE_EXIT_PARKING_SYMBOL=BOXX
31+
IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE=
32+
IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE=
33+
IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW=
2634

2735
# Optional email channel for escalated strategy plugin alerts.
2836
STRATEGY_PLUGIN_ALERT_EMAIL_RECIPIENTS=

.github/workflows/sync-cloud-run-env.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ jobs:
120120
INCOME_LAYER_ENABLED: ${{ vars.INCOME_LAYER_ENABLED }}
121121
INCOME_LAYER_START_USD: ${{ vars.INCOME_LAYER_START_USD }}
122122
INCOME_LAYER_MAX_RATIO: ${{ vars.INCOME_LAYER_MAX_RATIO }}
123+
DCA_MODE: ${{ vars.DCA_MODE }}
124+
DCA_BASE_INVESTMENT_USD: ${{ vars.DCA_BASE_INVESTMENT_USD }}
125+
IBIT_ZSCORE_EXIT_ENABLED: ${{ vars.IBIT_ZSCORE_EXIT_ENABLED }}
126+
IBIT_ZSCORE_EXIT_MODE: ${{ vars.IBIT_ZSCORE_EXIT_MODE }}
127+
IBIT_ZSCORE_EXIT_PARKING_SYMBOL: ${{ vars.IBIT_ZSCORE_EXIT_PARKING_SYMBOL }}
128+
IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE: ${{ vars.IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE }}
129+
IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE: ${{ vars.IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE }}
130+
IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW: ${{ vars.IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW }}
123131
RUNTIME_TARGET_ENABLED: ${{ vars.RUNTIME_TARGET_ENABLED }}
124132
EXECUTION_REPORT_GCS_URI: ${{ vars.EXECUTION_REPORT_GCS_URI }}
125133
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}
@@ -589,6 +597,14 @@ jobs:
589597
add_optional_env INCOME_LAYER_ENABLED
590598
add_optional_env INCOME_LAYER_START_USD
591599
add_optional_env INCOME_LAYER_MAX_RATIO
600+
add_optional_env DCA_MODE
601+
add_optional_env DCA_BASE_INVESTMENT_USD
602+
add_optional_env IBIT_ZSCORE_EXIT_ENABLED
603+
add_optional_env IBIT_ZSCORE_EXIT_MODE
604+
add_optional_env IBIT_ZSCORE_EXIT_PARKING_SYMBOL
605+
add_optional_env IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE
606+
add_optional_env IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE
607+
add_optional_env IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW
592608
add_optional_env RUNTIME_TARGET_ENABLED
593609
add_optional_env EXECUTION_REPORT_GCS_URI
594610
add_optional_env GLOBAL_TELEGRAM_CHAT_ID

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ flask
22
gunicorn
33
firstrade==0.0.39
44
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@46ca4ea3de8f98a58e2dd86158e7f2070d085cd1
5-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@173b8abbabc6a281b4707e100d248eae99e1f9d5
5+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ced1f78827e6112292af24d32dfe0e0f009e2833
66
google-cloud-storage
77
google-auth
88
requests

runtime_config_support.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ class PlatformRuntimeSettings:
5858
income_layer_max_ratio: float | None = None
5959
dca_mode: str | None = None
6060
dca_base_investment_usd: float | None = None
61+
ibit_zscore_exit_enabled: bool | None = None
62+
ibit_zscore_exit_mode: str | None = None
63+
ibit_zscore_exit_parking_symbol: str | None = None
64+
ibit_zscore_exit_risk_reduced_exposure: float | None = None
65+
ibit_zscore_exit_risk_off_exposure: float | None = None
66+
ibit_zscore_exit_allow_outside_execution_window: bool | None = None
6167
runtime_execution_window_trading_days: int | None = None
6268
market_signal_handoff_index_uri: str | None = None
6369
market_signal_handoff_manifest_uri: str | None = None
@@ -186,6 +192,16 @@ def load_platform_runtime_settings(
186192
income_layer_max_ratio=_optional_ratio_env("INCOME_LAYER_MAX_RATIO"),
187193
dca_mode=_optional_dca_mode_env("DCA_MODE"),
188194
dca_base_investment_usd=_optional_positive_float_env("DCA_BASE_INVESTMENT_USD"),
195+
ibit_zscore_exit_enabled=_optional_bool_env("IBIT_ZSCORE_EXIT_ENABLED"),
196+
ibit_zscore_exit_mode=_optional_ibit_zscore_exit_mode_env("IBIT_ZSCORE_EXIT_MODE"),
197+
ibit_zscore_exit_parking_symbol=_optional_symbol_env("IBIT_ZSCORE_EXIT_PARKING_SYMBOL"),
198+
ibit_zscore_exit_risk_reduced_exposure=_optional_ratio_env(
199+
"IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE"
200+
),
201+
ibit_zscore_exit_risk_off_exposure=_optional_ratio_env("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE"),
202+
ibit_zscore_exit_allow_outside_execution_window=_optional_bool_env(
203+
"IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW"
204+
),
189205
runtime_execution_window_trading_days=_runtime_execution_window_trading_days_env(
190206
strategy_definition.profile
191207
),
@@ -407,6 +423,37 @@ def _optional_dca_mode_env(name: str) -> str | None:
407423
return mode
408424

409425

426+
def _optional_ibit_zscore_exit_mode_env(name: str) -> str | None:
427+
raw_value = os.getenv(name)
428+
if raw_value is None or str(raw_value).strip() == "":
429+
return None
430+
value = str(raw_value).strip().lower()
431+
aliases = {
432+
"off": "disabled",
433+
"none": "disabled",
434+
"false": "disabled",
435+
"disable": "disabled",
436+
"enabled": "live",
437+
"shadow": "paper",
438+
"dry_run": "paper",
439+
"dry-run": "paper",
440+
}
441+
mode = aliases.get(value, value)
442+
if mode not in {"disabled", "paper", "live"}:
443+
raise ValueError(f"{name} must be disabled, paper, or live, got {raw_value!r}")
444+
return mode
445+
446+
447+
def _optional_symbol_env(name: str) -> str | None:
448+
raw_value = os.getenv(name)
449+
if raw_value is None or str(raw_value).strip() == "":
450+
return None
451+
value = str(raw_value).strip().upper()
452+
if len(value) > 16 or not value.replace(".", "").replace("-", "").isalnum():
453+
raise ValueError(f"{name} must be a symbol")
454+
return value
455+
456+
410457
def _resolve_non_negative_float_env(name: str, *, default: float) -> float:
411458
value = resolve_optional_float_env(os.environ, name)
412459
if value is None:

strategy_runtime.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
_FEATURE_SNAPSHOT_INPUT = "feature_snapshot"
2828
DCA_PROFILES = frozenset({"nasdaq_sp500_smart_dca", "ibit_smart_dca"})
29+
IBIT_ZSCORE_EXIT_PROFILE = "ibit_smart_dca"
2930

3031

3132
@dataclass(frozen=True)
@@ -183,6 +184,7 @@ def _build_runtime_overrides(profile: str, runtime_settings: PlatformRuntimeSett
183184
overrides["smart_multiplier_enabled"] = dca_mode == "smart"
184185
if dca_base_investment_usd is not None:
185186
overrides["base_investment_usd"] = dca_base_investment_usd
187+
_apply_ibit_zscore_exit_runtime_overrides(profile, runtime_settings, overrides)
186188
if profile == "tqqq_growth_income":
187189
if runtime_settings.income_threshold_usd is not None:
188190
overrides["income_threshold_usd"] = runtime_settings.income_threshold_usd
@@ -199,6 +201,29 @@ def _build_runtime_overrides(profile: str, runtime_settings: PlatformRuntimeSett
199201
return overrides
200202

201203

204+
def _apply_ibit_zscore_exit_runtime_overrides(
205+
profile: str,
206+
runtime_settings: PlatformRuntimeSettings,
207+
overrides: dict[str, Any],
208+
) -> None:
209+
if profile != IBIT_ZSCORE_EXIT_PROFILE:
210+
return
211+
for setting_name, override_name in (
212+
("ibit_zscore_exit_enabled", "ibit_zscore_exit_enabled"),
213+
("ibit_zscore_exit_mode", "ibit_zscore_exit_mode"),
214+
("ibit_zscore_exit_parking_symbol", "ibit_zscore_exit_parking_symbol"),
215+
("ibit_zscore_exit_risk_reduced_exposure", "ibit_zscore_exit_risk_reduced_exposure"),
216+
("ibit_zscore_exit_risk_off_exposure", "ibit_zscore_exit_risk_off_exposure"),
217+
(
218+
"ibit_zscore_exit_allow_outside_execution_window",
219+
"ibit_zscore_exit_allow_outside_execution_window",
220+
),
221+
):
222+
value = getattr(runtime_settings, setting_name, None)
223+
if value is not None:
224+
overrides[override_name] = value
225+
226+
202227
def load_strategy_runtime(
203228
raw_profile: str | None,
204229
*,

tests/test_runtime_config_support.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ def test_income_layer_overrides_load_from_env(monkeypatch):
110110
assert settings.income_layer_max_ratio == 0.25
111111

112112

113+
def test_ibit_zscore_exit_overrides_load_from_env(monkeypatch):
114+
monkeypatch.setenv("RUNTIME_TARGET_JSON", _target_json("ibit_smart_dca"))
115+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_ENABLED", "true")
116+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_MODE", "enabled")
117+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_PARKING_SYMBOL", "boxx")
118+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE", "0.5")
119+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE", "0.25")
120+
monkeypatch.setenv("IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW", "true")
121+
122+
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
123+
124+
assert settings.ibit_zscore_exit_enabled is True
125+
assert settings.ibit_zscore_exit_mode == "live"
126+
assert settings.ibit_zscore_exit_parking_symbol == "BOXX"
127+
assert settings.ibit_zscore_exit_risk_reduced_exposure == 0.5
128+
assert settings.ibit_zscore_exit_risk_off_exposure == 0.25
129+
assert settings.ibit_zscore_exit_allow_outside_execution_window is True
130+
131+
113132
def test_invalid_income_layer_max_ratio_is_rejected(monkeypatch):
114133
monkeypatch.setenv("RUNTIME_TARGET_JSON", _target_json("tqqq_growth_income"))
115134
monkeypatch.setenv("INCOME_LAYER_MAX_RATIO", "1.5")

tests/test_strategy_runtime.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ def test_dca_overrides_apply_to_runtime_config():
7777
}
7878

7979

80+
def test_ibit_zscore_exit_overrides_apply_to_runtime_config():
81+
settings = _runtime_settings(
82+
strategy_profile="ibit_smart_dca",
83+
ibit_zscore_exit_enabled=True,
84+
ibit_zscore_exit_mode="live",
85+
ibit_zscore_exit_parking_symbol="BOXX",
86+
ibit_zscore_exit_risk_reduced_exposure=0.5,
87+
ibit_zscore_exit_risk_off_exposure=0.25,
88+
ibit_zscore_exit_allow_outside_execution_window=True,
89+
)
90+
91+
assert _build_runtime_overrides("ibit_smart_dca", settings) == {
92+
"ibit_zscore_exit_enabled": True,
93+
"ibit_zscore_exit_mode": "live",
94+
"ibit_zscore_exit_parking_symbol": "BOXX",
95+
"ibit_zscore_exit_risk_reduced_exposure": 0.5,
96+
"ibit_zscore_exit_risk_off_exposure": 0.25,
97+
"ibit_zscore_exit_allow_outside_execution_window": True,
98+
}
99+
100+
80101
def test_reserved_cash_policy_overrides_apply_to_runtime_config():
81102
settings = _runtime_settings(
82103
strategy_profile="soxl_soxx_trend_income",

tests/test_sync_cloud_run_env_workflow.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def test_sync_cloud_run_env_workflow_syncs_strategy_plugin_alert_settings():
4949
"INCOME_LAYER_ENABLED",
5050
"INCOME_LAYER_START_USD",
5151
"INCOME_LAYER_MAX_RATIO",
52+
"DCA_MODE",
53+
"DCA_BASE_INVESTMENT_USD",
54+
"IBIT_ZSCORE_EXIT_ENABLED",
55+
"IBIT_ZSCORE_EXIT_MODE",
56+
"IBIT_ZSCORE_EXIT_PARKING_SYMBOL",
57+
"IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE",
58+
"IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE",
59+
"IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW",
5260
"FIRSTRADE_MARKET_SIGNAL_HANDOFF_INDEX_URI",
5361
"FIRSTRADE_MARKET_SIGNAL_HANDOFF_MANIFEST_URI",
5462
"FIRSTRADE_MARKET_SIGNAL_CONSUMPTION_AUDIT_URI",

0 commit comments

Comments
 (0)