Skip to content

Commit fb790de

Browse files
authored
Merge pull request #333 from QuantStrategyLab/fix/legacy-continuity-console-sync
fix: register eligible legacy continuity routes
2 parents 16065ad + 67581e6 commit fb790de

3 files changed

Lines changed: 313 additions & 18 deletions

File tree

.github/workflows/manual-strategy-switch.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,19 +574,27 @@ jobs:
574574
target = json.load(handle)
575575
runtime_target = target["runtime_target"]
576576
github = target["github"]
577+
continuity = runtime_target.get("live_continuity")
578+
if not isinstance(continuity, dict):
579+
continuity = {}
577580
payload = {
578581
"platform": runtime_target["platform_id"],
579582
"target_name": target["target_id"].split("/", 1)[1],
580583
"strategy_profile": runtime_target["strategy_profile"],
581584
"execution_mode": runtime_target["execution_mode"],
585+
"live_continuity_state": continuity.get("state", "NONE"),
582586
"variable_scope": "default",
583587
"plugin_mode": os.environ["PLUGIN_MODE"],
584588
"option_overlay_mode": os.environ.get("OPTION_OVERLAY_MODE", "current"),
589+
"cash_only_execution_mode": "current",
585590
"deployment_selector": runtime_target["deployment_selector"],
586591
"account_selector": ",".join(runtime_target["account_selector"]),
587592
"account_scope": runtime_target["account_scope"],
588593
"service_name": runtime_target["service_name"],
589594
}
595+
if continuity.get("state") and continuity.get("state") != "NONE":
596+
payload["live_continuity_baseline_id"] = continuity.get("baseline_id", "")
597+
payload["live_continuity_captured_at"] = continuity.get("captured_at", "")
590598
extra_variables_json = os.environ.get("EXTRA_VARIABLES_JSON", "").strip()
591599
if extra_variables_json:
592600
try:

tests/strategy_switch_worker_validation.mjs

Lines changed: 116 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,20 @@ const strategyProfiles = __test.normalizeStrategyProfilesPayload(
405405
can_switch_live: true,
406406
allowed_execution_modes: ["live", "dry_run"],
407407
},
408+
{
409+
profile: "legacy_continuity_profile",
410+
label: "Legacy continuity profile",
411+
domain: "us_equity",
412+
runtime_enabled: false,
413+
lifecycle_stage: "research_active",
414+
can_switch_live: false,
415+
allowed_execution_modes: ["dry_run"],
416+
blocked_live_reason: "candidate_gate_remains_closed",
417+
live_continuity: {
418+
eligible: true,
419+
allowed_platforms: ["ibkr"],
420+
},
421+
},
408422
{
409423
profile: "us_equity_combo_leveraged",
410424
label: "US Alpha Combo",
@@ -470,17 +484,19 @@ assert.equal(strategyProfiles[0].option_growth_overlay_nav_budget_ratio, "0.03")
470484
assert.equal(strategyProfiles[0].option_income_overlay_enabled, false);
471485
assert.equal(strategyProfiles[0].latest_evidence_status, "live_allowed");
472486
assert.equal(strategyProfiles[0].plugin_gate_status, "live_allowed");
473-
assert.equal(strategyProfiles[2].lifecycle_stage, "research_active");
474-
assert.equal(strategyProfiles[2].can_switch_live, false);
475-
assert.deepEqual(strategyProfiles[2].allowed_execution_modes, ["dry_run"]);
476-
assert.equal(strategyProfiles[2].blocked_live_reason, "promotion_required");
477-
assert.equal(strategyProfiles[2].latest_evidence_status, "research_only");
478-
assert.equal(strategyProfiles[2].plugin_gate_status, "blocked");
479-
assert.equal(strategyProfiles[3].dca_enabled, true);
480-
assert.equal(strategyProfiles[3].dca_default_mode, "fixed");
481-
assert.equal(strategyProfiles[3].dca_default_base_investment_usd, "1000");
482-
assert.equal(strategyProfiles[4].lifecycle_stage, "shadow_active");
483-
assert.equal(strategyProfiles[5].lifecycle_stage, "live_enabled");
487+
const legacyContinuityProfile = strategyProfiles.find((item) => item.profile === "legacy_continuity_profile");
488+
assert.deepEqual(legacyContinuityProfile.live_continuity, { eligible: true, allowed_platforms: ["ibkr"] });
489+
assert.equal(strategyProfiles[3].lifecycle_stage, "research_active");
490+
assert.equal(strategyProfiles[3].can_switch_live, false);
491+
assert.deepEqual(strategyProfiles[3].allowed_execution_modes, ["dry_run"]);
492+
assert.equal(strategyProfiles[3].blocked_live_reason, "promotion_required");
493+
assert.equal(strategyProfiles[3].latest_evidence_status, "research_only");
494+
assert.equal(strategyProfiles[3].plugin_gate_status, "blocked");
495+
assert.equal(strategyProfiles[4].dca_enabled, true);
496+
assert.equal(strategyProfiles[4].dca_default_mode, "fixed");
497+
assert.equal(strategyProfiles[4].dca_default_base_investment_usd, "1000");
498+
assert.equal(strategyProfiles[5].lifecycle_stage, "shadow_active");
499+
assert.equal(strategyProfiles[6].lifecycle_stage, "live_enabled");
484500

485501
assert.doesNotThrow(() =>
486502
__test.assertStrategyAllowedForAccount(
@@ -644,6 +660,92 @@ assert.deepEqual(kvUnboundSyncBody.account_options_sync, {
644660
skipped: true,
645661
});
646662

663+
const legacyContinuityKv = new Map([
664+
["account_options", JSON.stringify(accountOptions)],
665+
["strategy_profiles", JSON.stringify(strategyProfiles)],
666+
]);
667+
const legacyContinuityEnv = {
668+
STRATEGY_SWITCH_SYNC_TOKEN: "test-sync-token",
669+
STRATEGY_SWITCH_CONFIG: {
670+
get: async (key) => legacyContinuityKv.get(key) || null,
671+
put: async (key, value) => legacyContinuityKv.set(key, value),
672+
},
673+
};
674+
const legacyContinuityPayload = {
675+
platform: "ibkr",
676+
target_name: "legacy-ibkr-route",
677+
account_selector: "LEGACY_IBKR",
678+
deployment_selector: "legacy-ibkr-route",
679+
account_scope: "legacy-ibkr-route",
680+
service_name: "interactive-brokers-legacy-ibkr-route-service",
681+
strategy_profile: "legacy_continuity_profile",
682+
execution_mode: "live",
683+
live_continuity_state: "RECONCILE_ONLY",
684+
live_continuity_baseline_id: "legacy-ibkr-lkg-20260830",
685+
live_continuity_captured_at: "2026-08-30",
686+
variable_scope: "default",
687+
plugin_mode: "current",
688+
option_overlay_mode: "current",
689+
cash_only_execution_mode: "current",
690+
};
691+
const legacyContinuitySyncResponse = await worker.fetch(
692+
new Request("https://switch.example/api/internal/sync-account-default", {
693+
method: "POST",
694+
headers: {
695+
Authorization: "Bearer test-sync-token",
696+
"Content-Type": "application/json",
697+
},
698+
body: JSON.stringify(legacyContinuityPayload),
699+
}),
700+
legacyContinuityEnv,
701+
);
702+
assert.equal(legacyContinuitySyncResponse.status, 200);
703+
const legacyContinuitySyncBody = await legacyContinuitySyncResponse.json();
704+
assert.equal(legacyContinuitySyncBody.ok, true);
705+
assert.equal(legacyContinuitySyncBody.legacy_continuity_account_registered, true);
706+
const registeredLegacyAccount = JSON.parse(legacyContinuityKv.get("account_options")).ibkr.find(
707+
(option) => option.target_name === "legacy-ibkr-route",
708+
);
709+
assert.equal(registeredLegacyAccount.service_name, "interactive-brokers-legacy-ibkr-route-service");
710+
assert.deepEqual(registeredLegacyAccount.supported_domains, ["us_equity"]);
711+
assert.equal("plugin_mode" in registeredLegacyAccount, false);
712+
const repeatedLegacyContinuitySyncResponse = await worker.fetch(
713+
new Request("https://switch.example/api/internal/sync-account-default", {
714+
method: "POST",
715+
headers: {
716+
Authorization: "Bearer test-sync-token",
717+
"Content-Type": "application/json",
718+
},
719+
body: JSON.stringify(legacyContinuityPayload),
720+
}),
721+
legacyContinuityEnv,
722+
);
723+
assert.equal(repeatedLegacyContinuitySyncResponse.status, 200);
724+
assert.equal((await repeatedLegacyContinuitySyncResponse.json()).legacy_continuity_account_registered, false);
725+
assert.equal(
726+
JSON.parse(legacyContinuityKv.get("account_options")).ibkr.filter(
727+
(option) => option.target_name === "legacy-ibkr-route",
728+
).length,
729+
1,
730+
);
731+
const normalizedLegacyContinuityInputs = __test.normalizeSwitchInputs(legacyContinuityPayload);
732+
assert.doesNotThrow(() =>
733+
__test.assertStrategyAllowedForAccount(
734+
normalizedLegacyContinuityInputs,
735+
registeredLegacyAccount,
736+
strategyProfiles,
737+
),
738+
);
739+
assert.throws(
740+
() =>
741+
__test.assertStrategyAllowedForAccount(
742+
{ ...normalizedLegacyContinuityInputs, live_continuity_state: "NONE" },
743+
registeredLegacyAccount,
744+
strategyProfiles,
745+
),
746+
/not live-enabled/,
747+
);
748+
647749
const kvUnboundProfileSyncResponse = await worker.fetch(
648750
new Request("https://switch.example/api/internal/sync-strategy-profiles", {
649751
method: "POST",
@@ -722,6 +824,9 @@ assert.deepEqual(JSON.parse(normalizedCashOnlyInputs.extra_variables_json), {
722824
assert.equal("cash_only_execution_mode" in normalizedCashOnlyInputs, false);
723825

724826
const workflowYaml = readFileSync(resolve(root, ".github/workflows/manual-strategy-switch.yml"), "utf8");
827+
assert.ok(workflowYaml.includes('"live_continuity_state": continuity.get("state", "NONE")'));
828+
assert.ok(workflowYaml.includes('payload["live_continuity_baseline_id"]'));
829+
assert.ok(workflowYaml.includes('"cash_only_execution_mode": "current"'));
725830
const workflowInputs = [...workflowYaml.matchAll(/^ ([A-Za-z0-9_]+):\n description:/gm)].map((match) => match[1]);
726831
const dispatchInputs = __test.normalizeSwitchInputs({
727832
platform: "ibkr",

0 commit comments

Comments
 (0)