Add hosted synthetic control plane
This commit is contained in:
parent
af04875107
commit
8dcbcd2201
21 changed files with 3695 additions and 772 deletions
19
apps/web/app/api/admin/synthetic/control/route.ts
Normal file
19
apps/web/app/api/admin/synthetic/control/route.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { proxySyntheticAdminRequest } from "../shared";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
return proxySyntheticAdminRequest("/admin/synthetic/control", {
|
||||
method: "GET"
|
||||
});
|
||||
}
|
||||
|
||||
export async function PUT(req: Request): Promise<Response> {
|
||||
return proxySyntheticAdminRequest(
|
||||
"/admin/synthetic/control",
|
||||
{
|
||||
method: "PUT",
|
||||
body: await req.text()
|
||||
}
|
||||
);
|
||||
}
|
||||
61
apps/web/app/api/admin/synthetic/routes.test.ts
Normal file
61
apps/web/app/api/admin/synthetic/routes.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
||||
import {
|
||||
getSyntheticAdminProxyConfig,
|
||||
isSyntheticAdminFeatureEnabled
|
||||
} from "./shared";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
describe("synthetic admin proxy helpers", () => {
|
||||
beforeEach(() => {
|
||||
process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN = "1";
|
||||
process.env.NEXT_PUBLIC_API_URL = "http://127.0.0.1:4000";
|
||||
process.env.SYNTHETIC_ADMIN_TOKEN = "secret-token";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("gates visibility on the public env flag", () => {
|
||||
expect(isSyntheticAdminFeatureEnabled("1")).toBe(true);
|
||||
expect(isSyntheticAdminFeatureEnabled("0")).toBe(false);
|
||||
});
|
||||
|
||||
it("reads the proxy config from server env only", () => {
|
||||
expect(getSyntheticAdminProxyConfig()).toEqual({
|
||||
apiBaseUrl: "http://127.0.0.1:4000",
|
||||
token: "secret-token"
|
||||
});
|
||||
});
|
||||
|
||||
it("proxies status requests with the backend admin token", async () => {
|
||||
const fetchMock = mock(async (input: string | URL, init?: RequestInit) => {
|
||||
expect(String(input)).toBe("http://127.0.0.1:4000/admin/synthetic/status");
|
||||
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer secret-token");
|
||||
return new Response(JSON.stringify({ enabled: true }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
});
|
||||
});
|
||||
globalThis.fetch = fetchMock as typeof fetch;
|
||||
const route = await import("./status/route");
|
||||
|
||||
const response = await route.GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({ enabled: true });
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("returns 404 from proxy routes when the internal UI flag is off", async () => {
|
||||
process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN = "0";
|
||||
const route = await import("./control/route");
|
||||
|
||||
const response = await route.GET();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
63
apps/web/app/api/admin/synthetic/shared.ts
Normal file
63
apps/web/app/api/admin/synthetic/shared.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const jsonResponse = (body: unknown, status = 200): Response => {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const isSyntheticAdminFeatureEnabled = (
|
||||
value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN
|
||||
): boolean => value === "1";
|
||||
|
||||
export const getSyntheticAdminProxyConfig = (
|
||||
env: Record<string, string | undefined> = process.env
|
||||
): { apiBaseUrl: string; token: string } | null => {
|
||||
const apiBaseUrl = env.NEXT_PUBLIC_API_URL?.trim();
|
||||
const token = env.SYNTHETIC_ADMIN_TOKEN?.trim();
|
||||
if (!apiBaseUrl || !token) {
|
||||
return null;
|
||||
}
|
||||
return { apiBaseUrl, token };
|
||||
};
|
||||
|
||||
export const proxySyntheticAdminRequest = async (
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
env: Record<string, string | undefined> = process.env
|
||||
): Promise<Response> => {
|
||||
if (!isSyntheticAdminFeatureEnabled(env.NEXT_PUBLIC_SYNTHETIC_ADMIN)) {
|
||||
return jsonResponse({ error: "not found" }, 404);
|
||||
}
|
||||
|
||||
const config = getSyntheticAdminProxyConfig(env);
|
||||
if (!config) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "synthetic admin proxy misconfigured"
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(path, config.apiBaseUrl);
|
||||
const headers = new Headers(init.headers);
|
||||
headers.set("authorization", `Bearer ${config.token}`);
|
||||
if (!headers.has("content-type") && init.body) {
|
||||
headers.set("content-type", "application/json");
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
...init,
|
||||
cache: "no-store",
|
||||
headers
|
||||
});
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: {
|
||||
"content-type": response.headers.get("content-type") ?? "application/json"
|
||||
}
|
||||
});
|
||||
};
|
||||
9
apps/web/app/api/admin/synthetic/status/route.ts
Normal file
9
apps/web/app/api/admin/synthetic/status/route.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { proxySyntheticAdminRequest } from "../shared";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
return proxySyntheticAdminRequest("/admin/synthetic/status", {
|
||||
method: "GET"
|
||||
});
|
||||
}
|
||||
|
|
@ -1507,6 +1507,196 @@ h3 {
|
|||
z-index: 40;
|
||||
}
|
||||
|
||||
.synthetic-control-gear {
|
||||
position: fixed;
|
||||
right: 22px;
|
||||
bottom: 22px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(245, 166, 35, 0.24);
|
||||
border-radius: 12px;
|
||||
background: rgba(9, 13, 18, 0.96);
|
||||
color: var(--accent);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.38);
|
||||
z-index: 45;
|
||||
transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease;
|
||||
}
|
||||
|
||||
.synthetic-control-gear:hover,
|
||||
.synthetic-control-gear.is-open {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(245, 166, 35, 0.4);
|
||||
background: rgba(12, 18, 24, 0.98);
|
||||
}
|
||||
|
||||
.synthetic-control-gear-mark {
|
||||
display: inline-flex;
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.synthetic-control-drawer {
|
||||
position: fixed;
|
||||
top: 84px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: min(388px, calc(100vw - 20px));
|
||||
padding: 18px 18px 24px;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 16px;
|
||||
overflow: auto;
|
||||
border-left: 1px solid rgba(245, 166, 35, 0.18);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(245, 166, 35, 0.04), transparent 18%),
|
||||
rgba(6, 9, 13, 0.98);
|
||||
box-shadow: -18px 0 50px rgba(0, 0, 0, 0.34);
|
||||
z-index: 42;
|
||||
}
|
||||
|
||||
.synthetic-control-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.synthetic-control-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.synthetic-control-kicker {
|
||||
margin: 0 0 6px;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 0.64rem;
|
||||
}
|
||||
|
||||
.synthetic-control-section {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 14px 14px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.synthetic-control-section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--text-faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.synthetic-control-select select,
|
||||
.synthetic-segment,
|
||||
.synthetic-control-toggle {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.synthetic-control-select select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.synthetic-control-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.synthetic-control-toggle input {
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.synthetic-segment-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.synthetic-segment {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.synthetic-segment.is-active {
|
||||
border-color: rgba(245, 166, 35, 0.44);
|
||||
background: rgba(245, 166, 35, 0.12);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.synthetic-profile-grid,
|
||||
.synthetic-hit-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.synthetic-profile-row,
|
||||
.synthetic-hit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.synthetic-profile-row > span,
|
||||
.synthetic-hit-row > span,
|
||||
.synthetic-status-grid span {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.synthetic-status-grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.synthetic-status-grid strong,
|
||||
.synthetic-hit-row strong {
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.synthetic-control-disabled {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 14px 14px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.synthetic-control-disabled p,
|
||||
.synthetic-control-disabled span {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.synthetic-control-disabled-label {
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.synthetic-control-error {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
|
@ -1732,4 +1922,19 @@ h3 {
|
|||
max-height: none;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.synthetic-control-gear {
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
}
|
||||
|
||||
.synthetic-control-drawer {
|
||||
top: auto;
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
bottom: 68px;
|
||||
width: auto;
|
||||
border: 1px solid rgba(245, 166, 35, 0.16);
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
mergeNewestWithOverflow,
|
||||
normalizeAlertSeverity,
|
||||
nextFlowFilterPopoverState,
|
||||
isSyntheticAdminVisible,
|
||||
prunePinnedEntries,
|
||||
projectPausableTapeState,
|
||||
reducePausableTapeData,
|
||||
|
|
@ -407,6 +408,13 @@ describe("terminal navigation", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("synthetic admin visibility", () => {
|
||||
it("shows the internal control rail only when the public admin flag is enabled", () => {
|
||||
expect(isSyntheticAdminVisible("1")).toBe(true);
|
||||
expect(isSyntheticAdminVisible("0")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("live tape pausable helpers", () => {
|
||||
it("queues new items while paused and flushes them on resume", () => {
|
||||
let state = reducePausableTapeData(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,11 @@ import type {
|
|||
OptionNBBO,
|
||||
OptionPrint,
|
||||
SmartMoneyEvent,
|
||||
SmartMoneyProfileId
|
||||
SmartMoneyProfileId,
|
||||
SyntheticControlState,
|
||||
SyntheticCoverageWindowMinutes,
|
||||
SyntheticDerivedStatus,
|
||||
SyntheticProfileWeightValue
|
||||
} from "@islandflow/types";
|
||||
import {
|
||||
getSubscriptionKey as getLiveSubscriptionKey,
|
||||
|
|
@ -988,6 +992,96 @@ const buildApiUrl = (path: string): string => {
|
|||
return `${httpProtocol}://${host}${path}`;
|
||||
};
|
||||
|
||||
export const isSyntheticAdminVisible = (
|
||||
value = process.env.NEXT_PUBLIC_SYNTHETIC_ADMIN
|
||||
): boolean => value === "1";
|
||||
|
||||
type SyntheticAdminStatusResponse = {
|
||||
enabled: boolean;
|
||||
backend_mode: "synthetic" | "mixed" | "live";
|
||||
adapters: {
|
||||
options: string;
|
||||
equities: string;
|
||||
};
|
||||
control: SyntheticControlState | null;
|
||||
derived: SyntheticDerivedStatus | null;
|
||||
disabled_reason?: string;
|
||||
};
|
||||
|
||||
type SyntheticAdminControlResponse = {
|
||||
control: SyntheticControlState;
|
||||
derived?: SyntheticDerivedStatus | null;
|
||||
};
|
||||
|
||||
const SYNTHETIC_ADMIN_PROXY_PATHS = {
|
||||
status: "/api/admin/synthetic/status",
|
||||
control: "/api/admin/synthetic/control"
|
||||
} as const;
|
||||
|
||||
const SYNTHETIC_PROFILE_ORDER: Array<keyof SyntheticControlState["profile_weights"]> = [
|
||||
"institutional_directional",
|
||||
"retail_whale",
|
||||
"event_driven",
|
||||
"vol_seller",
|
||||
"arbitrage",
|
||||
"hedge_reactive"
|
||||
];
|
||||
|
||||
const SYNTHETIC_PROFILE_LABELS: Record<
|
||||
keyof SyntheticControlState["profile_weights"],
|
||||
string
|
||||
> = {
|
||||
institutional_directional: "Institutional Directional",
|
||||
retail_whale: "Retail Whale",
|
||||
event_driven: "Event Driven",
|
||||
vol_seller: "Vol Seller",
|
||||
arbitrage: "Arbitrage",
|
||||
hedge_reactive: "Hedge Reactive"
|
||||
};
|
||||
|
||||
const SYNTHETIC_PRESET_LABELS: Record<SyntheticControlState["preset_id"], string> = {
|
||||
balanced_demo: "Balanced Demo",
|
||||
event_day: "Event Day",
|
||||
dealer_day: "Dealer Day",
|
||||
retail_chase: "Retail Chase",
|
||||
quiet_range: "Quiet Range"
|
||||
};
|
||||
|
||||
const buildDefaultSyntheticControl = (): SyntheticControlState => ({
|
||||
preset_id: "balanced_demo",
|
||||
coverage_assist: true,
|
||||
coverage_window_minutes: 20,
|
||||
shared_seed: 11,
|
||||
profile_weights: {
|
||||
institutional_directional: 1.0,
|
||||
retail_whale: 1.0,
|
||||
event_driven: 1.0,
|
||||
vol_seller: 1.0,
|
||||
arbitrage: 1.0,
|
||||
hedge_reactive: 1.0
|
||||
},
|
||||
updated_at: 0,
|
||||
updated_by: "internal-ui"
|
||||
});
|
||||
|
||||
type SyntheticControlPatch = Omit<Partial<SyntheticControlState>, "profile_weights"> & {
|
||||
profile_weights?: Partial<SyntheticControlState["profile_weights"]>;
|
||||
};
|
||||
|
||||
const createSyntheticControlDraft = (
|
||||
current: SyntheticControlState,
|
||||
patch: SyntheticControlPatch
|
||||
): SyntheticControlState => ({
|
||||
...current,
|
||||
...patch,
|
||||
profile_weights: {
|
||||
...current.profile_weights,
|
||||
...(patch.profile_weights ?? {})
|
||||
},
|
||||
updated_at: Date.now(),
|
||||
updated_by: "internal-ui"
|
||||
});
|
||||
|
||||
const formatPrice = (price: number): string => {
|
||||
if (!Number.isFinite(price)) {
|
||||
return "0.00";
|
||||
|
|
@ -7926,6 +8020,331 @@ const ReplayConsole = memo(({ state }: { state: TerminalState }) => {
|
|||
);
|
||||
});
|
||||
|
||||
function SyntheticControlDock() {
|
||||
const visible = isSyntheticAdminVisible();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [status, setStatus] = useState<SyntheticAdminStatusResponse | null>(null);
|
||||
const [draft, setDraft] = useState<SyntheticControlState | null>(null);
|
||||
const [saved, setSaved] = useState<SyntheticControlState | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const dirtyRef = useRef(false);
|
||||
const savedRef = useRef<SyntheticControlState | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
try {
|
||||
const response = await fetch(SYNTHETIC_ADMIN_PROXY_PATHS.status, {
|
||||
cache: "no-store"
|
||||
});
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (response.status === 404) {
|
||||
setStatus({
|
||||
enabled: false,
|
||||
backend_mode: "live",
|
||||
adapters: { options: "unknown", equities: "unknown" },
|
||||
control: null,
|
||||
derived: null,
|
||||
disabled_reason: "Synthetic admin backend is disabled."
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const nextStatus = (await response.json()) as SyntheticAdminStatusResponse;
|
||||
setStatus(nextStatus);
|
||||
if (!dirtyRef.current) {
|
||||
const nextControl = nextStatus.control ?? buildDefaultSyntheticControl();
|
||||
setDraft(nextControl);
|
||||
setSaved(nextControl);
|
||||
savedRef.current = nextControl;
|
||||
}
|
||||
} catch (loadError) {
|
||||
if (!cancelled) {
|
||||
setError(loadError instanceof Error ? loadError.message : String(loadError));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
const timer = setInterval(() => {
|
||||
void load();
|
||||
}, 5_000);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !status?.enabled || !draft || !dirtyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const nextDraft = draft;
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
void fetch(SYNTHETIC_ADMIN_PROXY_PATHS.control, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(nextDraft)
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null);
|
||||
throw new Error(body?.detail ?? body?.error ?? "Synthetic control update failed");
|
||||
}
|
||||
return (await response.json()) as SyntheticAdminControlResponse;
|
||||
})
|
||||
.then((payload) => {
|
||||
dirtyRef.current = false;
|
||||
savedRef.current = payload.control;
|
||||
setSaved(payload.control);
|
||||
setDraft(payload.control);
|
||||
setStatus((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
control: payload.control,
|
||||
derived: payload.derived ?? current.derived
|
||||
}
|
||||
: current
|
||||
);
|
||||
})
|
||||
.catch((updateError) => {
|
||||
dirtyRef.current = false;
|
||||
setError(updateError instanceof Error ? updateError.message : String(updateError));
|
||||
setDraft(savedRef.current);
|
||||
})
|
||||
.finally(() => {
|
||||
setSaving(false);
|
||||
});
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [draft, status?.enabled, visible]);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentControl = draft ?? saved ?? buildDefaultSyntheticControl();
|
||||
const disabled = !status?.enabled;
|
||||
const derived = status?.derived;
|
||||
|
||||
const updateControl = (
|
||||
patch: SyntheticControlPatch
|
||||
) => {
|
||||
dirtyRef.current = true;
|
||||
setDraft((current) =>
|
||||
createSyntheticControlDraft(current ?? buildDefaultSyntheticControl(), patch)
|
||||
);
|
||||
};
|
||||
|
||||
const updateProfileWeight = (
|
||||
profileId: keyof SyntheticControlState["profile_weights"],
|
||||
value: SyntheticProfileWeightValue
|
||||
) => {
|
||||
updateControl({
|
||||
profile_weights: {
|
||||
[profileId]: value
|
||||
} as Partial<SyntheticControlState["profile_weights"]>
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
aria-expanded={open}
|
||||
aria-label="Synthetic control"
|
||||
className={`synthetic-control-gear${open ? " is-open" : ""}`}
|
||||
onClick={() => setOpen((current) => !current)}
|
||||
type="button"
|
||||
>
|
||||
<span className="synthetic-control-gear-mark">+</span>
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<aside className="synthetic-control-drawer" aria-label="Synthetic control drawer">
|
||||
<div className="synthetic-control-header">
|
||||
<div>
|
||||
<p className="synthetic-control-kicker">Synthetic Control</p>
|
||||
<h3>Hosted tape operator rail</h3>
|
||||
</div>
|
||||
<button className="drawer-close" onClick={() => setOpen(false)} type="button">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p className="drawer-note">Loading hosted synthetic status…</p>
|
||||
) : disabled ? (
|
||||
<div className="synthetic-control-disabled">
|
||||
<p className="synthetic-control-disabled-label">Unavailable</p>
|
||||
<p>{status?.disabled_reason ?? "Synthetic control is currently unavailable."}</p>
|
||||
<span>
|
||||
Backend: {status?.backend_mode ?? "unknown"} · Options:{" "}
|
||||
{status?.adapters.options ?? "unknown"} · Equities:{" "}
|
||||
{status?.adapters.equities ?? "unknown"}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<section className="synthetic-control-section">
|
||||
<div className="synthetic-control-section-head">
|
||||
<span>Preset</span>
|
||||
<span>{saving ? "Saving…" : "Live"}</span>
|
||||
</div>
|
||||
<label className="synthetic-control-select">
|
||||
<select
|
||||
onChange={(event) =>
|
||||
updateControl({
|
||||
preset_id: event.target.value as SyntheticControlState["preset_id"]
|
||||
})
|
||||
}
|
||||
value={currentControl.preset_id}
|
||||
>
|
||||
{Object.entries(SYNTHETIC_PRESET_LABELS).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="synthetic-control-section">
|
||||
<div className="synthetic-control-section-head">
|
||||
<span>Coverage</span>
|
||||
<span>{currentControl.coverage_window_minutes}m window</span>
|
||||
</div>
|
||||
<label className="synthetic-control-toggle">
|
||||
<input
|
||||
checked={currentControl.coverage_assist}
|
||||
onChange={(event) =>
|
||||
updateControl({ coverage_assist: event.target.checked })
|
||||
}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>Coverage assist</span>
|
||||
</label>
|
||||
<div className="synthetic-segment-row">
|
||||
{[10, 20, 30].map((minutes) => (
|
||||
<button
|
||||
className={`synthetic-segment${currentControl.coverage_window_minutes === minutes ? " is-active" : ""}`}
|
||||
key={minutes}
|
||||
onClick={() =>
|
||||
updateControl({
|
||||
coverage_window_minutes:
|
||||
minutes as SyntheticCoverageWindowMinutes
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{minutes}m
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="synthetic-control-section">
|
||||
<div className="synthetic-control-section-head">
|
||||
<span>Profile Bias</span>
|
||||
<span>Low · Normal · High</span>
|
||||
</div>
|
||||
<div className="synthetic-profile-grid">
|
||||
{SYNTHETIC_PROFILE_ORDER.map((profileId) => (
|
||||
<div className="synthetic-profile-row" key={profileId}>
|
||||
<span>{SYNTHETIC_PROFILE_LABELS[profileId]}</span>
|
||||
<div className="synthetic-segment-row">
|
||||
{[
|
||||
{ label: "Low", value: 0.6 },
|
||||
{ label: "Normal", value: 1.0 },
|
||||
{ label: "High", value: 1.6 }
|
||||
].map((option) => (
|
||||
<button
|
||||
className={`synthetic-segment${currentControl.profile_weights[profileId] === option.value ? " is-active" : ""}`}
|
||||
key={option.label}
|
||||
onClick={() =>
|
||||
updateProfileWeight(
|
||||
profileId,
|
||||
option.value as SyntheticProfileWeightValue
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="synthetic-control-section">
|
||||
<div className="synthetic-control-section-head">
|
||||
<span>Live Status</span>
|
||||
<span>{status?.backend_mode ?? "unknown"}</span>
|
||||
</div>
|
||||
<div className="synthetic-status-grid">
|
||||
<div>
|
||||
<span>Regime</span>
|
||||
<strong>{derived?.regime ?? "—"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Session</span>
|
||||
<strong>{derived?.session_phase ?? "—"}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Focus</span>
|
||||
<strong>
|
||||
{derived?.focus_symbols?.length
|
||||
? derived.focus_symbols.join(", ")
|
||||
: "—"}
|
||||
</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Backend</span>
|
||||
<strong>{status?.enabled ? "Enabled" : "Disabled"}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="synthetic-hit-list">
|
||||
{SYNTHETIC_PROFILE_ORDER.map((profileId) => (
|
||||
<div className="synthetic-hit-row" key={profileId}>
|
||||
<span>{SYNTHETIC_PROFILE_LABELS[profileId]}</span>
|
||||
<strong>{derived?.profile_hit_counts?.[profileId] ?? 0}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{error ? <p className="drawer-note synthetic-control-error">{error}</p> : null}
|
||||
</>
|
||||
)}
|
||||
</aside>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TerminalAppShell({ children }: { children: ReactNode }) {
|
||||
const state = useTerminalState();
|
||||
const pathname = usePathname();
|
||||
|
|
@ -8003,6 +8422,8 @@ export function TerminalAppShell({ children }: { children: ReactNode }) {
|
|||
<main className="terminal-content">{children}</main>
|
||||
</div>
|
||||
|
||||
<SyntheticControlDock />
|
||||
|
||||
{state.selectedAlert ? (
|
||||
<AlertDrawer
|
||||
alert={state.selectedAlert}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue