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
36 changes: 13 additions & 23 deletions scripts/build_cloud_run_env_sync_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,7 @@ def _should_add_local_src(candidate: Path) -> bool:
"probe_time": "CLOUD_SCHEDULER_PROBE_TIME",
"precheck_time": "CLOUD_SCHEDULER_PRECHECK_TIME",
}
NEAR_RUN_WARMUP_SCHEDULE = "43 9,15 * * 1-5"
RUN_SCHEDULER_ATTEMPT_DEADLINE = "330s"
NEAR_RUN_WARMUP_SERVICES = frozenset(
{
"interactive-brokers-quant-live-u15998061-service",
"interactive-brokers-quant-live-u16608560-service",
"interactive-brokers-quant-live-u18336562-service",
"interactive-brokers-quant-live-u18308207-service",
}
)

# Strategy-derived vars: auto-populated from platform-config.json defaults.
def _derive_strategy_env_defaults(strategy_config: dict) -> dict[str, str]:
Expand Down Expand Up @@ -468,9 +459,8 @@ def _build_target_plan(
env_values=env_values,
per_service_mode=per_service_mode,
)
if service_name in NEAR_RUN_WARMUP_SERVICES:
if _requires_extended_run_deadline(runtime_target, env_values):
scheduler["attempt_deadline"] = RUN_SCHEDULER_ATTEMPT_DEADLINE
_validate_near_run_warmup_schedule(service_name, scheduler)

return {
"service_name": service_name,
Expand Down Expand Up @@ -512,18 +502,18 @@ def _build_scheduler_plan(
return scheduler


def _validate_near_run_warmup_schedule(
service_name: str,
scheduler: Mapping[str, str],
) -> None:
if service_name not in NEAR_RUN_WARMUP_SERVICES:
return
if scheduler.get("timezone") != "America/New_York":
raise ValueError(f"{service_name} warmup timezone must be America/New_York")
if scheduler.get("probe_time") != NEAR_RUN_WARMUP_SCHEDULE:
raise ValueError(
f"{service_name} warmup schedule must be {NEAR_RUN_WARMUP_SCHEDULE!r}"
)
def _requires_extended_run_deadline(
runtime_target: Mapping[str, object],
env_values: Mapping[str, str],
) -> bool:
"""Reserve enough scheduler time for any live target backed by IB Gateway."""
backend = str(env_values.get("IBKR_EXECUTION_BACKEND") or "gateway").strip().lower()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive the deadline backend from the account-group configuration

For targets whose account-group secret sets execution_backend, this check can classify the service differently from the runtime. load_platform_runtime_settings gives group_config.execution_backend precedence over IBKR_EXECUTION_BACKEND (runtime_config_support.py:294-299), and the rollout documentation explicitly says this field is no longer primarily read from Cloud Run env. Thus, for example, a target/env value of quantconnect with a Gateway-backed account group omits the 330-second deadline even though the deployed /run handler uses IB Gateway; conversely a QuantConnect group with no env override is treated as Gateway by the fallback. Carry the resolved per-account-group backend into the sync target/plan rather than inferring capability from this env value.

Useful? React with 👍 / 👎.

raw_dry_run = runtime_target.get("dry_run_only")
if raw_dry_run is None:
raw_dry_run = env_values.get("IBKR_DRY_RUN_ONLY")
dry_run_only = str(raw_dry_run or "").strip().lower() in {"1", "true", "yes", "on"}
execution_mode = str(runtime_target.get("execution_mode") or "").strip().lower()
return backend == "gateway" and execution_mode != "paper" and not dry_run_only


def _validate_profile_inputs(
Expand Down
33 changes: 26 additions & 7 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,7 @@ def test_build_cloud_run_env_sync_plan_supports_per_service_targets():
"main_time": "10 16",
"probe_time": "40 9,15",
"precheck_time": "45 9",
"attempt_deadline": "330s",
}
assert "IBKR_FEATURE_SNAPSHOT_PATH" not in slot_a["env"]
assert "IBKR_FEATURE_SNAPSHOT_PATH" in slot_a["remove_env_vars"]
Expand All @@ -1151,6 +1152,7 @@ def test_build_cloud_run_env_sync_plan_supports_per_service_targets():
"main_time": "45 15 1-7 * *",
"probe_time": "35 9,15 1-7 * *",
"precheck_time": "45 9 1-7 * *",
"attempt_deadline": "330s",
}
assert u7654_mega["env"]["IBKR_FEATURE_SNAPSHOT_PATH"] == "gs://runtime/mega/snapshot.csv"
assert (
Expand Down Expand Up @@ -1225,7 +1227,7 @@ def _four_gateway_warmup_payload(probe_time: str) -> dict[str, object]:
"runtime_target": {
**json.loads(
runtime_target_json(
"us_equity_combo_leveraged",
"tqqq_growth_income",
dry_run_only=True,
deployment_selector="us-combo-shadow",
account_scope="us-combo-shadow",
Expand All @@ -1252,7 +1254,7 @@ def _four_gateway_warmup_payload(probe_time: str) -> dict[str, object]:
}


def test_build_cloud_run_env_sync_plan_generates_four_gateway_near_run_warmups() -> None:
def test_build_cloud_run_env_sync_plan_generates_live_gateway_deadlines() -> None:
payload = _four_gateway_warmup_payload("43 9,15 * * 1-5")
result = subprocess.run(
[sys.executable, str(SYNC_PLAN_SCRIPT_PATH), "--json"],
Expand Down Expand Up @@ -1284,19 +1286,36 @@ def test_build_cloud_run_env_sync_plan_generates_four_gateway_near_run_warmups()
assert "attempt_deadline" not in by_service["interactive-brokers-us-combo-shadow-service"]["scheduler"]


def test_build_cloud_run_env_sync_plan_rejects_stale_gateway_warmup_schedule() -> None:
def test_build_cloud_run_env_sync_plan_accepts_strategy_defined_gateway_schedule() -> None:
payload = _four_gateway_warmup_payload("35 9,15 * * 1-5")
first_target = payload["targets"][0]
first_target["runtime_target"]["scheduler"] = {
"timezone": "Asia/Hong_Kong",
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5",
}
result = subprocess.run(
[sys.executable, str(SYNC_PLAN_SCRIPT_PATH), "--json"],
check=False,
check=True,
capture_output=True,
text=True,
env={**os.environ, "CLOUD_RUN_SERVICE_TARGETS_JSON": json.dumps(payload)},
)

assert result.returncode != 0
assert "interactive-brokers-quant-live-u15998061-service" in result.stderr
assert "43 9,15 * * 1-5" in result.stderr
plan = json.loads(result.stdout)
first_plan = next(
target
for target in plan["targets"]
if target["service_name"] == "interactive-brokers-quant-live-u15998061-service"
)
assert first_plan["scheduler"] == {
"timezone": "Asia/Hong_Kong",
"main_time": "45 15 * * 1-5",
"probe_time": "35 9,15 * * 1-5",
"precheck_time": "45 9 * * 1-5",
"attempt_deadline": "330s",
}


def test_build_cloud_run_env_sync_plan_requires_target_snapshot_in_per_service_mode():
Expand Down
22 changes: 14 additions & 8 deletions tests/test_scheduler_deadline_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,22 @@
WORKFLOW = ROOT / ".github" / "workflows" / "sync-cloud-run-env.yml"


def test_four_gateway_run_deadline_exceeds_cloud_run_timeout() -> None:
expected_services = {
"interactive-brokers-quant-live-u15998061-service",
"interactive-brokers-quant-live-u16608560-service",
"interactive-brokers-quant-live-u18336562-service",
"interactive-brokers-quant-live-u18308207-service",
}
assert sync_plan.NEAR_RUN_WARMUP_SERVICES == expected_services
def test_live_gateway_run_deadline_exceeds_cloud_run_timeout() -> None:
assert sync_plan.RUN_SCHEDULER_ATTEMPT_DEADLINE == "330s"

assert sync_plan._requires_extended_run_deadline(
{"execution_mode": "live", "dry_run_only": False},
{"IBKR_EXECUTION_BACKEND": "gateway"},
)
assert not sync_plan._requires_extended_run_deadline(
{"execution_mode": "paper", "dry_run_only": True},
{"IBKR_EXECUTION_BACKEND": "gateway"},
)
assert not sync_plan._requires_extended_run_deadline(
{"execution_mode": "live", "dry_run_only": False},
{"IBKR_EXECUTION_BACKEND": "quantconnect"},
)

workflow = WORKFLOW.read_text(encoding="utf-8")
cloud_run_timeout = re.search(r"--timeout=(\d+)s", workflow)
assert cloud_run_timeout is not None
Expand Down
Loading