Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependencies = [
"google-cloud-storage",
"yfinance",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@0063af3b4a974650ea58a7d3f26dd1b94f65d3e8",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@46887bc3f5454d5b59623b1f5efb7c65912c6b8b",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@cd959e30e91e57697ad878d98799c1afb43bf5fb",
"hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@61993bf261aeccf64b5a75428b9405f4e1d1d682",
]

Expand Down
4 changes: 2 additions & 2 deletions scripts/print_strategy_switch_env_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@ def build_switch_plan(
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"] = (
hints["shadow_402040_strategy_config_path"] = (
"package://us_equity_strategies/configs/"
"us_equity_combo_leveraged_shadow_352045.json"
"us_equity_combo_leveraged_shadow_402040.json"
)

return {
Expand Down
111 changes: 78 additions & 33 deletions strategy_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,50 +469,95 @@ def _build_direct_market_data_inputs(
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()
raw_regime_symbols = self.merged_runtime_config.get("market_data_regime_symbols") or ("SPY", "QQQ", "SOXX")
if isinstance(raw_regime_symbols, str):
regime_symbols = tuple(
symbol.strip().upper()
for symbol in raw_regime_symbols.split(",")
if symbol.strip()
)
else:
regime_symbols = tuple(
str(symbol).strip().upper()
for symbol in raw_regime_symbols
if str(symbol).strip()
)
regime_symbols = tuple(dict.fromkeys((trend_symbol, *regime_symbols)))
ma_window = int(
self.merged_runtime_config.get(
"market_data_ma_window",
self.merged_runtime_config.get("sma_period", 200),
)
)
slope_window = int(self.merged_runtime_config.get("market_data_slope_window") or 20)
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)}"
if slope_window <= 1:
raise ValueError("market_data_slope_window must be greater than 1")

def load_close_series(symbol: str) -> pd.Series:
history = historical_close_loader(
ib,
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"),
)
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,
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 {symbol} closes, "
f"got {len(close_series)}"
)
return close_series

market_data: dict[str, Any] = {
"trend_symbol": trend_symbol,
"trend_symbols": regime_symbols,
"trend_ma_window": ma_window,
"trend_slope_window": slope_window,
"regime_indicators": {},
}
for symbol in regime_symbols:
close_series = load_close_series(symbol)
price = float(close_series.iloc[-1])
ma_value = float(close_series.tail(ma_window).mean())
rolling_ma = close_series.rolling(slope_window).mean()
slope_positive = bool(len(rolling_ma.dropna()) >= 2 and rolling_ma.iloc[-1] > rolling_ma.iloc[-2])
key = symbol.lower()
market_data[f"{key}_above_ma200"] = price > ma_value
market_data[f"{key}_price"] = price
market_data[f"{key}_ma200"] = ma_value
market_data[f"{key}_ma20_slope_positive"] = slope_positive
market_data["regime_indicators"][key] = {
"price": price,
"ma200": ma_value,
"above_ma200": price > ma_value,
"ma20_slope_positive": slope_positive,
"history_observation_count": int(len(close_series)),
}
if symbol == trend_symbol:
market_data.update(
{
"trend_price": price,
"trend_ma": ma_value,
"history_observation_count": int(len(close_series)),
}
)
return {
_MARKET_DATA_INPUT: market_data,
}

def _build_strategy_context(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ def test_print_strategy_switch_env_plan_keeps_optional_config_for_leveraged_comb
assert "IBKR_STRATEGY_CONFIG_PATH" in plan["optional_env"]
assert "IBKR_STRATEGY_CONFIG_PATH" not in plan["remove_if_present"]
assert "IBKR_FEATURE_SNAPSHOT_PATH" in plan["remove_if_present"]
assert plan["hints"]["shadow_352045_strategy_config_path"].startswith(
assert plan["hints"]["shadow_402040_strategy_config_path"].startswith(
"package://us_equity_strategies/"
)

Expand Down
18 changes: 17 additions & 1 deletion tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import sys
from pathlib import Path
from types import SimpleNamespace

ROOT = Path(__file__).resolve().parents[1]
QPK_SRC = ROOT.parent / "QuantPlatformKit" / "src"
UES_SRC = ROOT.parent / "UsEquityStrategies" / "src"
HES_SRC = ROOT.parent / "HkEquityStrategies" / "src"
for candidate in (ROOT, QPK_SRC, UES_SRC, HES_SRC):
if candidate.exists() and str(candidate) not in sys.path:
sys.path.insert(0, str(candidate))

import strategy_runtime as strategy_runtime_module
from quant_platform_kit.common.models import PortfolioSnapshot
from quant_platform_kit.strategy_contracts import (
Expand Down Expand Up @@ -409,7 +419,7 @@ def evaluate(self, ctx):
)

def fake_close_loader(_ib, symbol, **_kwargs):
if symbol == "SPY":
if symbol in {"SPY", "QQQ", "SOXX"}:
return strategy_runtime_module.pd.Series([100.0] * 200 + [120.0])
return strategy_runtime_module.pd.Series([10.0] * 201)

Expand All @@ -425,7 +435,13 @@ def fake_close_loader(_ib, symbol, **_kwargs):

market_data = captured["market_data"]["market_data"]
assert market_data["spy_above_ma200"] is True
assert market_data["qqq_above_ma200"] is True
assert market_data["soxx_above_ma200"] is True
assert market_data["spy_ma20_slope_positive"] is True
assert market_data["qqq_ma20_slope_positive"] is True
assert market_data["soxx_ma20_slope_positive"] is True
assert market_data["trend_symbol"] == "SPY"
assert market_data["trend_symbols"] == ("SPY", "QQQ", "SOXX")
assert market_data["trend_ma_window"] == 200
assert captured["runtime_config"]["market_data_ma_window"] == 200
assert result.metadata["managed_symbols"] == ("TQQQ", "SOXL", "BOXX")
Expand Down
4 changes: 2 additions & 2 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading