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
6 changes: 6 additions & 0 deletions python/scripts/execution_evidence_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
{
"not_due",
"no_action",
"no_signal",
"no_rebalance",
"risk_blocked",
"submitted",
"broker_acknowledged",
Expand All @@ -72,6 +74,8 @@
_EXECUTION_RECEIPT_OUTCOME_CONFIRMATIONS = {
"not_due": frozenset({"not_applicable"}),
"no_action": frozenset({"not_applicable"}),
"no_signal": frozenset({"not_applicable"}),
"no_rebalance": frozenset({"not_applicable"}),
"risk_blocked": frozenset({"not_applicable"}),
"submitted": frozenset({"not_observed"}),
"broker_acknowledged": frozenset({"acknowledged"}),
Expand Down Expand Up @@ -298,6 +302,8 @@ def _execution_evidence_from_receipt(
if receipt is None:
return "pending", "target_execution_evidence_missing"
outcome = receipt["outcome"]
if outcome in {"no_signal", "no_rebalance"}:
return "not_applicable", "target_execution_receipt_observed"
if outcome == "reconciliation_required":
return "unavailable", "target_execution_reconciliation_required"
if outcome == "failed":
Expand Down
23 changes: 23 additions & 0 deletions python/tests/test_execution_evidence_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def _execution_receipt(self, *, outcome: str = "filled") -> dict[str, str]:
"filled": "filled",
"failed": "not_observed",
"reconciliation_required": "reconciliation_required",
"no_signal": "not_applicable",
"no_rebalance": "not_applicable",
}[outcome]
observed_at = "2026-08-25T16:00:00Z"
return {
Expand Down Expand Up @@ -143,6 +145,27 @@ def test_projects_a_matching_minimal_execution_receipt_without_order_details(sel
for forbidden in ("receipt_id", "must-not-be-projected", "account_ids", "api_token", "gs://"):
self.assertNotIn(forbidden, serialized)

def test_accepts_non_action_receipts_without_granting_execution_authority(self):
for outcome in ("no_signal", "no_rebalance"):
with self.subTest(outcome=outcome):
report = self._report()
report["execution_receipt"] = self._execution_receipt(outcome=outcome)

snapshot = projection.build_execution_evidence_source_snapshot(
[report],
source_id="runtime-reports",
now=datetime(2026, 8, 25, 16, 5, tzinfo=UTC),
)

deployment = snapshot["deployments"][0]
self.assertEqual(deployment["evidence"]["target_execution"], "not_applicable")
self.assertEqual(deployment["recommendation"], {
"code": "parked",
"reason_code": "target_execution_receipt_observed",
})
self.assertEqual(deployment["execution_receipt"]["outcome"], outcome)
self.assertEqual(deployment["execution_receipt"]["broker_confirmation"], "not_applicable")

def test_rejects_a_tampered_execution_receipt_without_claiming_execution(self):
report = self._report()
receipt = self._execution_receipt()
Expand Down
12 changes: 8 additions & 4 deletions tests/console_runtime_state_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,18 @@ test('buildInputs keeps untouched policy layers current and serializes only touc
assert.equal(touched.min_reserved_cash_usd, '100');
});

test('account settings copy removes guesswork and keeps the two research views', () => {
test('account settings copy removes guesswork and prioritizes research candidates', () => {
const html = readFileSync(new URL('../web/strategy-switch-console/index.html', import.meta.url), 'utf8');
const app = readFileSync(new URL('../web/strategy-switch-console/app.js', import.meta.url), 'utf8');
assert.ok(html.includes('id="mode-display"'));
assert.ok(html.includes('id="execution-mode-select"'));
assert.equal(html.includes('data-mode="live"'), false);
assert.ok(html.includes('data-research-internal-view="monitoring"'));
assert.ok(html.includes('data-research-internal-view="research"'));
assert.equal(html.includes('research-internal-tabs'), false);
assert.equal(html.includes('data-research-internal-view'), false);
assert.ok(html.includes('id="monitoring-diagnostics" hidden'));
assert.ok(html.indexOf('id="promotion-decision-panel"') < html.indexOf('id="monitoring-diagnostics"'));
assert.equal(app.includes('researchInternalView'), false);
assert.ok(app.includes('if (state.view === "research") void refreshResearchWorkspace()'));
assert.equal(html.includes('需要启停或调整策略时展开'), false);
assert.equal(html.includes('插件、收入层和期权的选择不授予运行许可'), false);
assert.equal(app.includes('target {target} · service {service} · market {domains}'), false);
Expand Down Expand Up @@ -750,7 +754,7 @@ test('account details keep saved, deployed and application state separate', () =
const app = readFileSync(new URL('../web/strategy-switch-console/app.js', import.meta.url), 'utf8');
assert.ok(html.includes('data-i18n="overviewRuntime"'));
assert.ok(html.includes('data-i18n="latestReadback"'));
assert.ok(html.indexOf('data-research-internal-view="monitoring"') < html.indexOf('id="promotion-decision-panel"'));
assert.ok(html.indexOf('id="promotion-decision-panel"') < html.indexOf('id="monitoring-diagnostics"'));
assert.ok(html.includes('class="account-facts"'));
assert.ok(app.includes('["deployedSwitch", accountDeploymentText(platform, account)]'));
assert.ok(app.includes('["applicationStatus", application]'));
Expand Down
54 changes: 52 additions & 2 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ assert.ok(indexHtml.includes('switchSurface.classList.toggle("summary-hidden", !
assert.equal(indexHtml.match(/Generated by inject_platform_config\.py/g)?.length, 1);
assert.ok(indexHtml.includes('<script src="/bootstrap-config.js"></script>'));
assert.ok(indexHtml.includes('<script src="/boot-recovery.js"></script>'));
assert.ok(indexHtml.includes('/app.js?v=console-clarity-v1'));
assert.ok(indexHtml.includes('/app.css?v=console-clarity-v1'));
assert.ok(indexHtml.includes('/app.js?v=console-clarity-v2'));
assert.ok(indexHtml.includes('/app.css?v=console-clarity-v2'));
assert.equal(indexHtml.includes('<script id="platform-config">'), false);
assert.equal(indexHtml.includes("publicSummary"), false);
assert.ok(indexHtml.includes("function hasPrivateConfig()"));
Expand Down Expand Up @@ -3446,6 +3446,56 @@ assert.deepEqual(receiptDeployment.deployment.execution_receipt, {
observed_at: executionReceiptObservedAt,
});

for (const outcome of ["no_signal", "no_rebalance"]) {
const nonActionPayload = {
...executionReceiptEvidenceSourcePayload,
source_id: `longbridge.execution_receipt.${outcome}`,
deployments: [{
...executionReceiptEvidenceSourcePayload.deployments[0],
deployment_id: `soxl_soxx_trend_income.${outcome}.longbridge.paper`,
strategy: {
...executionReceiptEvidenceSourcePayload.deployments[0].strategy,
candidate_id: `soxl_soxx_trend_income.${outcome}`,
},
evidence: {
...executionReceiptEvidenceSourcePayload.deployments[0].evidence,
target_execution: "not_applicable",
},
execution_receipt: {
outcome,
broker_confirmation: "not_applicable",
observed_at: executionReceiptObservedAt,
},
}],
};
const syncResponse = await worker.fetch(
new Request("https://switch.example/api/internal/sync-execution-evidence-source", {
method: "POST",
headers: { Authorization: `Bearer ${executionEvidenceSyncValue}`, "Content-Type": "application/json" },
body: JSON.stringify(nonActionPayload),
}),
executionEvidenceEnv,
);
assert.equal(syncResponse.status, 200);
}
const nonActionEvidenceRead = await worker.fetch(
new Request("https://switch.example/api/execution-evidence", { headers: executionEvidenceCookieHeaders }),
executionEvidenceEnv,
);
const nonActionEvidencePayload = await nonActionEvidenceRead.json();
for (const outcome of ["no_signal", "no_rebalance"]) {
const deployment = nonActionEvidencePayload.deployments.find(
(entry) => entry.deployment.deployment_id === `soxl_soxx_trend_income.${outcome}.longbridge.paper`,
);
assert.equal(deployment.deployment.evidence.target_execution, "not_applicable");
assert.equal(deployment.deployment.recommendation.code, "parked");
assert.deepEqual(deployment.deployment.execution_receipt, {
outcome,
broker_confirmation: "not_applicable",
observed_at: executionReceiptObservedAt,
});
}

const runtimeTargetLifecycleSourcePayload = {
schema_version: "qsl_runtime_target_lifecycle_source_snapshot.v1",
source_id: "longbridge.sg",
Expand Down
48 changes: 19 additions & 29 deletions web/strategy-switch-console/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
--qmt: #b45309;
--bn: #f0b90b;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
"IBM Plex Sans", "PingFang SC", "Microsoft YaHei", ui-sans-serif,
system-ui, sans-serif;
}

* {
Expand Down Expand Up @@ -3328,7 +3328,22 @@
/* Daily workspace: overview first, account changes and research in separate views. */
.operator-console.console-clarity {
--ink: #172d35; --muted: #667680; --accent: #10766a; --accent-soft: #e9f4f2; --line: #dfe6ea;
background: #f6f7f9; color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: #f6f7f9; color: var(--ink);
font-family: "IBM Plex Sans", "PingFang SC", "Microsoft YaHei", ui-sans-serif, system-ui, sans-serif;
}
.operator-console.console-clarity .management-heading h2,
.operator-console.console-clarity .board-heading h3,
.operator-console.console-clarity .decision-head h2 {
font-family: "Source Serif 4", "Songti SC", "Noto Serif SC", Georgia, serif;
font-optical-sizing: auto;
}
@media (prefers-reduced-motion: reduce) {
.operator-console.console-clarity *,
.operator-console.console-clarity *::before,
.operator-console.console-clarity *::after {
animation: none !important;
transition: none !important;
}
}
.console-clarity .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
.operator-console.console-clarity .topbar { position: fixed; inset: 0 auto 0 0; width: 232px; height: 100dvh; padding: 30px 16px 24px; display: flex; flex-direction: column; align-items: stretch; gap: 32px; border: 0; border-right: 1px solid var(--line); background: #fafbfc; box-shadow: none; z-index: 20; }
Expand Down Expand Up @@ -3560,8 +3575,7 @@
/* Keep controls aligned without centering the page copy. */
.operator-console .btn,
.operator-console .run-button,
.operator-console .mode button,
.operator-console .research-internal-tab {
.operator-console .mode button {
display: inline-flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -3617,30 +3631,6 @@
to { transform: rotate(360deg); }
}

.operator-console .research-internal-tabs {
display: flex;
align-items: center;
gap: 6px;
padding: 4px;
border-bottom: 1px solid var(--line);
}

.operator-console .research-internal-tab {
min-height: 36px;
padding: 0 14px;
border: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
font-size: 13px;
font-weight: 700;
}

.operator-console .research-internal-tab.active {
background: var(--ink);
color: #fff;
}

.operator-console .mode-unread {
align-self: center;
}
Expand Down
52 changes: 21 additions & 31 deletions web/strategy-switch-console/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@
overviewNav: "运行总览",
accountsNav: "账户管理",
researchNav: "研究与确认",
overviewDescription: "查看已保存的配置、实际读回与最近运行记录。",
accountsDescription: "先查看当前账户状态,需要调整时再展开设置。",
researchDescription: "查看候选与待办,按需展开研究记录。",
overviewDescription: "先看有没有必须你确认的事,再扫账户是否异常;改配置放到账户管理。",
accountsDescription: "先看读回状态;要改开关或策略时再展开设置。提交只记意图,不等于成交。",
researchDescription: "只处理有材料的候选与晋级确认;接受意图不等于实盘授权。",
accountsTitle: "账户",
accountFilters: "筛选账户",
filterAll: "全部",
Expand Down Expand Up @@ -750,9 +750,9 @@
overviewNav: "Overview",
accountsNav: "Accounts",
researchNav: "Research & decisions",
overviewDescription: "Saved settings, actual readback, and recent runtime records.",
accountsDescription: "Review the account first. Expand settings when you need to make a change.",
researchDescription: "Review candidates and decisions. Open research records as needed.",
overviewDescription: "Check decisions first, then scan account anomalies. Change settings under Accounts.",
accountsDescription: "Review readback first. Expand settings only when changing. Submit records intent, not a fill.",
researchDescription: "Handle candidates with complete material only. Accepting intent is not live authority.",
accountsTitle: "Accounts",
accountFilters: "Filter accounts",
filterAll: "All",
Expand Down Expand Up @@ -1412,7 +1412,6 @@

const state = {
view: "overview",
researchInternalView: "monitoring",
overviewFilter: "all",
overviewSearch: "",
lastRefreshAt: null,
Expand Down Expand Up @@ -6421,14 +6420,7 @@
el("platform-strip-label").hidden = view !== "accounts";
el("platform-strip-label").textContent = t("activePlatform");
el("health-view").hidden = view !== "research";
document.querySelectorAll?.("[data-research-panel]")?.forEach((panel) => {
panel.hidden = panel.dataset.researchPanel !== state.researchInternalView;
});
document.querySelectorAll?.("[data-research-internal-view]")?.forEach((button) => {
const active = button.dataset.researchInternalView === state.researchInternalView;
button.classList?.toggle("active", active);
button.setAttribute?.("aria-selected", String(active));
});
el("monitoring-diagnostics").hidden = view !== "research";
el("workspace-title").textContent = t(`${view}Nav`);
el("workspace-description").textContent = t(`${view}Description`);
document.querySelector(".workspace-nav").hidden = !state.appReady || !state.auth.allowed;
Expand Down Expand Up @@ -7102,9 +7094,23 @@
");
}

function refreshResearchWorkspace() {
return Promise.allSettled([
refreshHealth(),
refreshReconciliationRecovery(),
refreshM0Research(),
refreshAdaptiveSelection(),
refreshExecutionEvidence(),
refreshResearchTasks(),
refreshResearchPromotionTickets(),
refreshRuntimeTargetLifecycle(),
]);
}

document.querySelectorAll("[data-workspace]").forEach(button => button.addEventListener("click", () => {
state.view = button.dataset.workspace;
render();
if (state.view === "research") void refreshResearchWorkspace();
el("workspace-title").focus({ preventScroll: true });
window.scrollTo({ top: 0 });
}));
Expand All @@ -7124,22 +7130,6 @@
renderOverview();
el("overview-search").focus();
});
document.querySelectorAll("[data-research-internal-view]").forEach((button) => button.addEventListener("click", () => {
state.researchInternalView = button.dataset.researchInternalView === "research" ? "research" : "monitoring";
renderWorkspace();
if (state.researchInternalView === "research") {
refreshHealth();
refreshReconciliationRecovery();
refreshM0Research();
refreshAdaptiveSelection();
refreshExecutionEvidence();
refreshResearchTasks();
refreshResearchPromotionTickets();
} else {
refreshRuntimeTargetLifecycle();
}
}));

document.querySelectorAll("[data-health-filter]").forEach((button) => button.addEventListener("click", () => {
document.querySelectorAll("[data-health-filter]").forEach((node) => node.classList.remove("active"));
button.classList.add("active");
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/app_css.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion web/strategy-switch-console/app_js.js

Large diffs are not rendered by default.

Loading