-
Notifications
You must be signed in to change notification settings - Fork 1
Support leveraged combo shadow runtime #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -245,10 +245,16 @@ def build_switch_plan( | |
| [ | ||
| "IBKR_FEATURE_SNAPSHOT_PATH", | ||
| "IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH", | ||
| "IBKR_STRATEGY_CONFIG_PATH", | ||
| "IBKR_RECONCILIATION_OUTPUT_PATH", | ||
| ] | ||
| ) | ||
| if config_source_policy == "env_only": | ||
| optional_env.append("IBKR_STRATEGY_CONFIG_PATH") | ||
| notes.append( | ||
| "IBKR_STRATEGY_CONFIG_PATH is optional for this profile; set it only for an explicit shadow/runtime config." | ||
| ) | ||
| else: | ||
| remove_if_present.append("IBKR_STRATEGY_CONFIG_PATH") | ||
|
|
||
| hints: dict[str, str] = {} | ||
| if requires_feature_snapshot: | ||
|
|
@@ -260,6 +266,11 @@ def build_switch_plan( | |
| hints["feature_snapshot_manifest_filename"] = manifest_filename | ||
| if artifact_paths.bundled_config_path is not None: | ||
| hints["bundled_strategy_config_path"] = str(artifact_paths.bundled_config_path) | ||
| if definition.profile == "us_equity_combo_leveraged": | ||
| hints["shadow_352045_strategy_config_path"] = ( | ||
| "package://us_equity_strategies/configs/" | ||
| "us_equity_combo_leveraged_shadow_352045.json" | ||
| ) | ||
|
Comment on lines
+269
to
+273
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This hint tells operators to set Useful? React with 👍 / 👎. |
||
|
|
||
| return { | ||
| "platform": IBKR_PLATFORM, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,7 @@ | |
| DCA_PROFILES = frozenset({"nasdaq_sp500_smart_dca", "ibit_smart_dca"}) | ||
| IBIT_ZSCORE_EXIT_PROFILE = "ibit_smart_dca" | ||
| _MARKET_HISTORY_INPUT = "market_history" | ||
| _MARKET_DATA_INPUT = "market_data" | ||
| _BENCHMARK_HISTORY_INPUT = "benchmark_history" | ||
| _DERIVED_INDICATORS_INPUT = "derived_indicators" | ||
| _PORTFOLIO_SNAPSHOT_INPUT = "portfolio_snapshot" | ||
|
|
@@ -459,6 +460,61 @@ def _build_market_history_inputs( | |
| } | ||
| } | ||
|
|
||
| def _build_direct_market_data_inputs( | ||
| self, | ||
| ib, | ||
| historical_close_loader: Callable[..., Any], | ||
| ) -> Mapping[str, Any]: | ||
| if self.profile != "us_equity_combo_leveraged": | ||
| raise ValueError(f"Unsupported market_data strategy profile {self.profile!r}") | ||
|
|
||
| trend_symbol = str(self.merged_runtime_config.get("market_data_trend_symbol") or "SPY").strip().upper() | ||
| ma_window = int( | ||
| self.merged_runtime_config.get( | ||
| "market_data_ma_window", | ||
| self.merged_runtime_config.get("sma_period", 200), | ||
| ) | ||
| ) | ||
| if ma_window <= 0: | ||
| raise ValueError("market_data_ma_window must be positive") | ||
| history = historical_close_loader( | ||
| ib, | ||
| trend_symbol, | ||
| duration=str(self.merged_runtime_config.get("market_data_history_duration") or "2 Y"), | ||
| bar_size=str(self.merged_runtime_config.get("market_data_history_bar_size") or "1 day"), | ||
| ) | ||
| if isinstance(history, pd.DataFrame): | ||
| if "close" in history.columns: | ||
| close_values = history["close"] | ||
| else: | ||
| close_values = history.iloc[:, 0] | ||
| elif isinstance(history, pd.Series): | ||
| close_values = history | ||
| else: | ||
| close_values = [ | ||
| item.get("close") if isinstance(item, Mapping) else getattr(item, "close", item) | ||
| for item in (history or ()) | ||
| ] | ||
| close_series = pd.to_numeric(pd.Series(close_values), errors="coerce").dropna() | ||
| close_series = close_series[close_series > 0] | ||
| if len(close_series) < ma_window: | ||
| raise ValueError( | ||
| f"{self.profile} requires at least {ma_window} positive {trend_symbol} closes, " | ||
| f"got {len(close_series)}" | ||
| ) | ||
| trend_price = float(close_series.iloc[-1]) | ||
| trend_ma = float(close_series.tail(ma_window).mean()) | ||
| return { | ||
| _MARKET_DATA_INPUT: { | ||
| "spy_above_ma200": trend_price > trend_ma, | ||
| "trend_symbol": trend_symbol, | ||
| "trend_price": trend_price, | ||
| "trend_ma": trend_ma, | ||
| "trend_ma_window": ma_window, | ||
| "history_observation_count": int(len(close_series)), | ||
| } | ||
| } | ||
|
|
||
| def _build_strategy_context( | ||
| self, | ||
| *, | ||
|
|
@@ -517,6 +573,16 @@ def evaluate( | |
| pacing_sec=pacing_sec, | ||
| strategy_plugin_signals=strategy_plugin_signals, | ||
| ) | ||
| if _MARKET_DATA_INPUT in self.required_inputs: | ||
| return self._evaluate_direct_market_data_strategy( | ||
| ib=ib, | ||
| current_holdings=current_holdings, | ||
| historical_close_loader=historical_close_loader, | ||
| run_as_of=run_as_of, | ||
| translator=translator, | ||
| pacing_sec=pacing_sec, | ||
| strategy_plugin_signals=strategy_plugin_signals, | ||
| ) | ||
| if _MARKET_HISTORY_INPUT in self.required_inputs: | ||
| return self._evaluate_market_data_strategy( | ||
| ib=ib, | ||
|
|
@@ -547,6 +613,81 @@ def evaluate( | |
| f"{', '.join(sorted(self.required_inputs)) or '<none>'}" | ||
| ) | ||
|
|
||
| def _evaluate_direct_market_data_strategy( | ||
| self, | ||
| *, | ||
| ib, | ||
| current_holdings, | ||
| historical_close_loader: Callable[..., Any], | ||
| run_as_of: pd.Timestamp, | ||
| translator: Callable[[str], str], | ||
| pacing_sec: float, | ||
| strategy_plugin_signals=(), | ||
| ) -> StrategyEvaluationResult: | ||
| runtime_config = dict(self.runtime_config) | ||
| runtime_config.setdefault("translator", translator) | ||
| runtime_config.setdefault("pacing_sec", float(pacing_sec)) | ||
| apply_runtime_policy_to_runtime_config(runtime_config, self.runtime_adapter) | ||
| portfolio_snapshot = self._fetch_portfolio_snapshot_for_context( | ||
| ib, | ||
| required=False, | ||
| ) | ||
| portfolio_snapshot = self._project_portfolio_snapshot( | ||
| portfolio_snapshot, | ||
| self._configured_strategy_symbols(include_ranking_pool=True), | ||
| ) | ||
| portfolio_snapshot = self._attach_strategy_plugin_metadata(portfolio_snapshot, strategy_plugin_signals) | ||
| option_chains = self._fetch_option_chains_for_runtime(ib, runtime_config, portfolio_snapshot) | ||
| if option_chains: | ||
| runtime_config["option_chains"] = option_chains | ||
| ctx = self._build_strategy_context( | ||
| runtime_adapter=self.runtime_adapter, | ||
| as_of=run_as_of, | ||
| market_inputs=self._build_direct_market_data_inputs(ib, historical_close_loader), | ||
| portfolio_snapshot=portfolio_snapshot, | ||
| runtime_config=runtime_config, | ||
| current_holdings=current_holdings, | ||
| ib=ib, | ||
| ) | ||
| decision = self.entrypoint.evaluate(ctx) | ||
| managed_symbols = tuple( | ||
| dict.fromkeys( | ||
| str(position.symbol).strip().upper() | ||
| for position in decision.positions | ||
| if str(position.symbol or "").strip() | ||
| ) | ||
|
Comment on lines
+653
to
+658
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the leveraged combo is risk-off and the strategy returns only the safe-haven target (for example Useful? React with 👍 / 👎. |
||
| ) | ||
| price_fallbacks = self._build_historical_close_map( | ||
| ib, | ||
| historical_close_loader, | ||
| self._build_price_fallback_symbol_list( | ||
| decision, | ||
| managed_symbols=managed_symbols, | ||
| current_holdings=current_holdings, | ||
| ), | ||
| ) | ||
| metadata = { | ||
| "strategy_profile": self.profile, | ||
| "managed_symbols": managed_symbols, | ||
| "status_icon": self.status_icon, | ||
| "dry_run_only": self.runtime_settings.dry_run_only, | ||
| **build_execution_timing_metadata( | ||
| signal_date=run_as_of, | ||
| signal_effective_after_trading_days=( | ||
| self.runtime_adapter.runtime_policy.signal_effective_after_trading_days | ||
| ), | ||
| ), | ||
| } | ||
| if portfolio_snapshot is not None: | ||
| metadata = self._enrich_portfolio_metadata(metadata, portfolio_snapshot) | ||
| if "BOXX" in managed_symbols: | ||
| metadata["safe_haven_symbol"] = "BOXX" | ||
| if price_fallbacks: | ||
| metadata["price_fallbacks"] = price_fallbacks | ||
| metadata["dry_run_price_fallbacks"] = price_fallbacks | ||
| metadata["price_fallback_source"] = "historical_close" | ||
| return StrategyEvaluationResult(decision=decision, metadata=metadata) | ||
|
|
||
| def _evaluate_market_data_strategy( | ||
| self, | ||
| *, | ||
|
|
@@ -942,7 +1083,10 @@ def load_strategy_runtime( | |
| logger=logger, | ||
| ) | ||
| runtime_config: dict[str, Any] = {} | ||
| if _FEATURE_SNAPSHOT_INPUT in frozenset(entrypoint.manifest.required_inputs): | ||
| if ( | ||
| _FEATURE_SNAPSHOT_INPUT in frozenset(entrypoint.manifest.required_inputs) | ||
| or runtime_settings.strategy_config_path | ||
| ): | ||
| runtime_config = runtime.load_runtime_parameters() | ||
| runtime_config.update(_build_runtime_overrides(runtime_settings)) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For non-snapshot
env_onlyprofiles this keepsIBKR_STRATEGY_CONFIG_PATHas optional instead of removing it, so a service that previously had the shadow package config set and is later switched with the default plan keeps using that value. Sincestrategy_runtime.pynow loads runtime parameters wheneverruntime_settings.strategy_config_pathis non-empty, the supposedly canonical runtime can continue to run the old shadow weights unless the operator explicitly replaces the path.Useful? React with 👍 / 👎.