implement durable options tape history
This commit is contained in:
parent
e3940eb0a6
commit
bd60d0d5d5
9 changed files with 423 additions and 56 deletions
|
|
@ -606,6 +606,13 @@ h3 {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.flow-filter-section-copy {
|
||||
margin: -2px 0 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.flow-filter-checkbox-grid,
|
||||
.flow-filter-chip-grid {
|
||||
display: grid;
|
||||
|
|
@ -617,6 +624,10 @@ h3 {
|
|||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flow-filter-chip-grid-two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flow-filter-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
getEffectiveOptionPrintFilters,
|
||||
getAlertWindowAnchorTs,
|
||||
getHotChannelFeedStatus,
|
||||
getScopedLiveAutoHydrationChannels,
|
||||
getLiveHistoryRetentionCap,
|
||||
getOptionTableSnapshot,
|
||||
getOptionScope,
|
||||
|
|
@ -298,6 +297,24 @@ describe("contract-focused option helpers", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("includes the selected options view in tape query params", () => {
|
||||
expect(
|
||||
buildOptionTapeQueryParams(
|
||||
{
|
||||
...buildDefaultFlowFilters(),
|
||||
view: "raw",
|
||||
securityTypes: undefined,
|
||||
nbboSides: undefined,
|
||||
optionTypes: undefined
|
||||
},
|
||||
{ underlying_ids: ["AAPL"] }
|
||||
)
|
||||
).toEqual({
|
||||
view: "raw",
|
||||
underlying_ids: "AAPL"
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the focus seed until the matching scoped subscription has loaded it", () => {
|
||||
const seedItem = makeOptionPrint({
|
||||
trace_id: "focused-seed",
|
||||
|
|
@ -652,32 +669,6 @@ describe("live tape history helpers", () => {
|
|||
).toBe(0);
|
||||
});
|
||||
|
||||
it("does not auto-hydrate scoped live history before the scroll gate is reached", () => {
|
||||
const manifest = getLiveManifest(
|
||||
"/tape",
|
||||
"AAPL",
|
||||
60000,
|
||||
buildDefaultFlowFilters(),
|
||||
{
|
||||
underlying_ids: ["AAPL"],
|
||||
option_contract_id: "AAPL-2025-01-17-200-C"
|
||||
},
|
||||
{ underlying_ids: ["AAPL"] }
|
||||
);
|
||||
const historyCursors = Object.fromEntries(
|
||||
manifest.map((subscription) => [getLiveSubscriptionKey(subscription), { ts: 1, seq: 1 }])
|
||||
);
|
||||
|
||||
expect(
|
||||
getScopedLiveAutoHydrationChannels(true, "/tape", manifest, historyCursors, {})
|
||||
).toEqual([]);
|
||||
expect(
|
||||
getScopedLiveAutoHydrationChannels(true, "/tape", manifest, historyCursors, {
|
||||
[getLiveSubscriptionKey(manifest.find((subscription) => subscription.channel === "options")!)]: true
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("restores the same anchor key after live insertions at the top", () => {
|
||||
const nextKeys = ["new-1", "new-2", "anchor", "after-1", "after-2"];
|
||||
expect(findAnchorRestoreIndex(nextKeys, "anchor", ["anchor", "after-1", "after-2"])).toBe(2);
|
||||
|
|
@ -806,6 +797,7 @@ describe("flow filter popup helpers", () => {
|
|||
|
||||
expect(countActiveFlowFilterGroups(defaults)).toBe(0);
|
||||
expect(countActiveFlowFilterGroups(next)).toBe(3);
|
||||
expect(countActiveFlowFilterGroups({ ...defaults, view: "raw" })).toBe(1);
|
||||
expect(buildDefaultFlowFilters()).toEqual(defaults);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import type {
|
|||
LiveHotChannelHealthMap,
|
||||
LiveSubscription,
|
||||
OptionFlowFilters,
|
||||
OptionFlowView,
|
||||
OptionNbboSide,
|
||||
OptionSecurityType,
|
||||
OptionType,
|
||||
|
|
@ -853,21 +854,6 @@ export const getLiveHistoryRetentionCap = (subscription: LiveSubscription): numb
|
|||
}
|
||||
};
|
||||
|
||||
export const getScopedLiveAutoHydrationChannels = (
|
||||
enabled: boolean,
|
||||
pathname: string,
|
||||
manifest: LiveSubscription[],
|
||||
historyCursors: Partial<Record<string, Cursor | null>>,
|
||||
historyLoading: Partial<Record<string, boolean>>
|
||||
): Array<Extract<LiveSubscription["channel"], "options" | "equities">> => {
|
||||
void enabled;
|
||||
void pathname;
|
||||
void manifest;
|
||||
void historyCursors;
|
||||
void historyLoading;
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getLiveFeedStatus = (
|
||||
sourceStatus: WsStatus,
|
||||
freshestTs: number | null,
|
||||
|
|
@ -1436,6 +1422,9 @@ export const countActiveFlowFilterGroups = (filters: OptionFlowFilters): number
|
|||
if ((filters.minNotional ?? undefined) !== (defaults.minNotional ?? undefined)) {
|
||||
count += 1;
|
||||
}
|
||||
if ((filters.view ?? defaults.view) !== defaults.view) {
|
||||
count += 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
};
|
||||
|
|
@ -3684,18 +3673,6 @@ const useLiveSession = (
|
|||
[enabled, manifest, historyCursors, historyLoading]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
for (const channel of getScopedLiveAutoHydrationChannels(
|
||||
enabled,
|
||||
pathname,
|
||||
manifest,
|
||||
historyCursors,
|
||||
historyLoading
|
||||
)) {
|
||||
void loadOlder(channel);
|
||||
}
|
||||
}, [enabled, pathname, manifest, historyCursors, historyLoading, loadOlder]);
|
||||
|
||||
return {
|
||||
status,
|
||||
connectedAt,
|
||||
|
|
@ -6904,6 +6881,17 @@ export const FlowFilterPopover = ({ filters, onChange }: FlowFilterPopoverProps)
|
|||
}));
|
||||
};
|
||||
|
||||
const applyView = (view: OptionFlowView) => {
|
||||
onChange((prev) => ({
|
||||
...prev,
|
||||
view,
|
||||
securityTypes: view === "raw" ? undefined : prev.securityTypes ?? DEFAULT_FLOW_SECURITY_TYPES,
|
||||
nbboSides: view === "raw" ? undefined : prev.nbboSides,
|
||||
optionTypes: view === "raw" ? undefined : prev.optionTypes,
|
||||
minNotional: view === "raw" ? undefined : prev.minNotional
|
||||
}));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
|
|
@ -6968,6 +6956,27 @@ export const FlowFilterPopover = ({ filters, onChange }: FlowFilterPopoverProps)
|
|||
</div>
|
||||
|
||||
<div className="flow-filter-popover-body">
|
||||
<FlowFilterSection title="Options View">
|
||||
<div className="flow-filter-chip-grid flow-filter-chip-grid-two">
|
||||
{[
|
||||
{ label: "Signal", value: "signal" as const },
|
||||
{ label: "All prints", value: "raw" as const }
|
||||
].map((preset) => (
|
||||
<button
|
||||
className={`filter-chip ${filters.view === preset.value ? "is-active" : ""}`}
|
||||
key={preset.value}
|
||||
type="button"
|
||||
onClick={() => applyView(preset.value)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="flow-filter-section-copy">
|
||||
Signal keeps classifier-ready prints. All prints includes raw option tape rows.
|
||||
</p>
|
||||
</FlowFilterSection>
|
||||
|
||||
<FlowFilterSection title="Security">
|
||||
<div className="flow-filter-checkbox-grid">
|
||||
{(["stock", "etf"] as OptionSecurityType[]).map((value) => (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue