Skip to content

Commit a89ca9d

Browse files
Pigbibicursoragent
andauthored
Preserve market signal runtime variables during strategy switch patching. (#79)
Strategy switch now keeps platform and generic MARKET_SIGNAL_* GitHub Variables when service target entries are replaced without explicit overrides. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5f200df commit a89ca9d

2 files changed

Lines changed: 75 additions & 0 deletions

File tree

scripts/build_runtime_switch.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@
9595
"INCOME_LAYER_START_USD",
9696
"INCOME_LAYER_MAX_RATIO",
9797
)
98+
MARKET_SIGNAL_RUNTIME_SUFFIXES = (
99+
"MARKET_SIGNAL_HANDOFF_INDEX_URI",
100+
"MARKET_SIGNAL_HANDOFF_MANIFEST_URI",
101+
"MARKET_SIGNAL_CONSUMPTION_AUDIT_URI",
102+
"MARKET_SIGNAL_CACHE_DIR",
103+
"MARKET_SIGNAL_REQUIRED",
104+
"MARKET_SIGNAL_FALLBACK_MODE",
105+
"MARKET_SIGNAL_MAX_STALE_DAYS",
106+
)
107+
PLATFORM_MARKET_SIGNAL_PREFIXES = {
108+
"schwab": "SCHWAB",
109+
"longbridge": "LONGBRIDGE",
110+
"ibkr": "IBKR",
111+
"firstrade": "FIRSTRADE",
112+
}
113+
MARKET_SIGNAL_RUNTIME_VARIABLES = tuple(MARKET_SIGNAL_RUNTIME_SUFFIXES) + tuple(
114+
f"{prefix}_{suffix}"
115+
for prefix in PLATFORM_MARKET_SIGNAL_PREFIXES.values()
116+
for suffix in MARKET_SIGNAL_RUNTIME_SUFFIXES
117+
)
98118
CASH_ONLY_EXECUTION_VARIABLE = "CASH_ONLY_EXECUTION"
99119
LEGACY_INCOME_LAYER_VARIABLES = (
100120
"INCOME_THRESHOLD_USD",
@@ -792,6 +812,7 @@ def _preserve_reserved_cash_fields(
792812
*RUNTIME_TARGET_VARIABLES,
793813
*DCA_RUNTIME_VARIABLES,
794814
*IBIT_ZSCORE_EXIT_RUNTIME_VARIABLES,
815+
*MARKET_SIGNAL_RUNTIME_VARIABLES,
795816
):
796817
if variable and variable not in replacement and variable in current_entry:
797818
replacement[variable] = current_entry[variable]

tests/test_runtime_settings.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,60 @@ def test_build_switch_target_preserves_dca_fields_in_service_targets_when_omitte
842842
self.assertEqual(selected["DCA_MODE"], "smart")
843843
self.assertEqual(selected["DCA_BASE_INVESTMENT_USD"], "500")
844844

845+
def test_build_switch_target_preserves_market_signal_fields_in_service_targets_when_omitted(self):
846+
existing = {
847+
"targets": [
848+
{
849+
"service": "interactive-brokers-demo-ibkr-dca-service",
850+
"ACCOUNT_GROUP": "demo-ibkr-dca",
851+
"IBKR_MARKET_SIGNAL_HANDOFF_INDEX_URI": "gs://signals/index.json",
852+
"IBKR_MARKET_SIGNAL_REQUIRED": "true",
853+
"IBKR_MARKET_SIGNAL_FALLBACK_MODE": "last_valid",
854+
"runtime_target": {
855+
"platform_id": "ibkr",
856+
"strategy_profile": "nasdaq_sp500_smart_dca",
857+
"dry_run_only": False,
858+
"deployment_selector": "demo-ibkr-dca",
859+
"account_selector": ["DEMO_IBKR_DCA"],
860+
"account_scope": "demo-ibkr-dca",
861+
"service_name": "interactive-brokers-demo-ibkr-dca-service",
862+
"execution_mode": "live",
863+
},
864+
},
865+
],
866+
}
867+
path = ROOT / ".pytest_runtime_service_targets_market_signal.json"
868+
path.write_text(runtime_settings.compact_json(existing), encoding="utf-8")
869+
self.addCleanup(lambda: path.unlink(missing_ok=True))
870+
parser = build_runtime_switch.build_parser()
871+
args = parser.parse_args(
872+
[
873+
"--platform",
874+
"ibkr",
875+
"--target-name",
876+
"demo-ibkr-dca",
877+
"--strategy-profile",
878+
"ibit_smart_dca",
879+
"--account-selector",
880+
"DEMO_IBKR_DCA",
881+
"--service-name",
882+
"interactive-brokers-demo-ibkr-dca-service",
883+
"--plugin-mode",
884+
"none",
885+
"--existing-service-targets-json-file",
886+
str(path),
887+
]
888+
)
889+
890+
target = build_runtime_switch.build_switch_target(args)
891+
assignments = {item.name: item.value for item in runtime_settings.build_assignments(target)}
892+
selected = json.loads(assignments["CLOUD_RUN_SERVICE_TARGETS_JSON"])["targets"][0]
893+
894+
self.assertEqual(selected["runtime_target"]["strategy_profile"], "ibit_smart_dca")
895+
self.assertEqual(selected["IBKR_MARKET_SIGNAL_HANDOFF_INDEX_URI"], "gs://signals/index.json")
896+
self.assertEqual(selected["IBKR_MARKET_SIGNAL_REQUIRED"], "true")
897+
self.assertEqual(selected["IBKR_MARKET_SIGNAL_FALLBACK_MODE"], "last_valid")
898+
845899
def test_build_switch_target_uses_snapshot_scheduler_window(self):
846900
parser = build_runtime_switch.build_parser()
847901
args = parser.parse_args(

0 commit comments

Comments
 (0)