Tune synthetic flow mix and stabilize second-row card lists

This commit is contained in:
dirtydishes 2025-12-30 11:31:06 -05:00
parent eda219852f
commit 15fce370ef
4 changed files with 290 additions and 165 deletions

View file

@ -373,6 +373,7 @@ h1 {
scrollbar-gutter: stable; scrollbar-gutter: stable;
} }
.row { .row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -793,31 +794,30 @@ h1 {
.card-flow, .card-flow,
.card-alerts, .card-alerts,
.card-classifiers { .card-classifiers {
display: grid; display: flex;
grid-template-columns: minmax(0, 1fr); flex-direction: column;
height: 760px; height: 960px;
overflow: hidden;
} }
.card-flow, .card-body {
.card-classifiers { display: flex;
grid-template-rows: auto auto minmax(0, 1fr); flex-direction: column;
} flex: 1 1 0;
.card-alerts {
grid-template-rows: auto auto auto minmax(0, 1fr);
}
.card-flow .list,
.card-alerts .list,
.card-classifiers .list {
height: 100%;
min-height: 0; min-height: 0;
gap: 16px;
}
.card-body .list {
flex: 1 1 0;
min-height: 0;
height: auto;
} }
@media (max-width: 720px) { @media (max-width: 720px) {
.card-flow, .card-flow,
.card-alerts, .card-alerts,
.card-classifiers { .card-classifiers {
height: 600px; height: 780px;
} }
} }

View file

@ -239,6 +239,22 @@ const useListScroll = (): ListScrollState => {
isAtTopRef.current = isAtTop; isAtTopRef.current = isAtTop;
}, [isAtTop]); }, [isAtTop]);
const updateScrollState = useCallback(() => {
const el = listRef.current;
if (!el) {
return;
}
const atTop = el.scrollTop <= 2;
isAtTopRef.current = atTop;
setIsAtTop(atTop);
if (atTop) {
setMissed(0);
}
}, [isAtTopRef]);
useEffect(() => { useEffect(() => {
const el = listRef.current; const el = listRef.current;
if (!el) { if (!el) {
@ -246,21 +262,16 @@ const useListScroll = (): ListScrollState => {
} }
const onScroll = () => { const onScroll = () => {
const atTop = el.scrollTop <= 2; updateScrollState();
isAtTopRef.current = atTop;
setIsAtTop(atTop);
if (atTop) {
setMissed(0);
}
}; };
onScroll(); updateScrollState();
el.addEventListener("scroll", onScroll); el.addEventListener("scroll", onScroll);
return () => { return () => {
el.removeEventListener("scroll", onScroll); el.removeEventListener("scroll", onScroll);
}; };
}, []); }, [updateScrollState]);
const onNewItems = useCallback((count: number) => { const onNewItems = useCallback((count: number) => {
if (count <= 0) { if (count <= 0) {
@ -281,9 +292,10 @@ const useListScroll = (): ListScrollState => {
return; return;
} }
el.scrollTo({ top: 0, behavior: "smooth" }); isAtTopRef.current = true;
setMissed(0); el.scrollTop = 0;
}, []); updateScrollState();
}, [isAtTopRef, listRef, updateScrollState]);
return { return {
listRef, listRef,
@ -299,10 +311,11 @@ const useScrollAnchor = (
listRef: React.RefObject<HTMLDivElement>, listRef: React.RefObject<HTMLDivElement>,
isAtTopRef: React.MutableRefObject<boolean> isAtTopRef: React.MutableRefObject<boolean>
) => { ) => {
const pendingRef = useRef<{ top: number; height: number } | null>(null); const pendingRef = useRef<{ height: number } | null>(null);
const capture = useCallback(() => { const capture = useCallback(() => {
if (isAtTopRef.current) { if (isAtTopRef.current) {
pendingRef.current = null;
return; return;
} }
@ -312,7 +325,6 @@ const useScrollAnchor = (
} }
pendingRef.current = { pendingRef.current = {
top: el.scrollTop,
height: el.scrollHeight height: el.scrollHeight
}; };
}, [isAtTopRef, listRef]); }, [isAtTopRef, listRef]);
@ -328,10 +340,17 @@ const useScrollAnchor = (
return; return;
} }
if (isAtTopRef.current) {
pendingRef.current = null;
return;
}
const delta = el.scrollHeight - pending.height; const delta = el.scrollHeight - pending.height;
el.scrollTop = pending.top + delta; if (delta !== 0) {
el.scrollTop = Math.max(0, el.scrollTop + delta);
}
pendingRef.current = null; pendingRef.current = null;
}, [listRef]); }, [isAtTopRef, listRef]);
return { capture, apply }; return { capture, apply };
}; };
@ -1624,48 +1643,50 @@ export default function HomePage() {
/> />
</div> </div>
<div className="list" ref={flowScroll.listRef}> <div className="card-body">
{mode !== "live" ? ( <div className="list" ref={flowScroll.listRef}>
<div className="empty">Flow packets are live-only in this build.</div> {mode !== "live" ? (
) : filteredFlow.length === 0 ? ( <div className="empty">Flow packets are live-only in this build.</div>
<div className="empty"> ) : filteredFlow.length === 0 ? (
{tickerSet.size > 0 <div className="empty">
? "No flow packets match the current filter." {tickerSet.size > 0
: "No flow packets yet. Start compute."} ? "No flow packets match the current filter."
</div> : "No flow packets yet. Start compute."}
) : ( </div>
filteredFlow.map((packet) => { ) : (
const features = packet.features ?? {}; filteredFlow.map((packet) => {
const contract = String(features.option_contract_id ?? packet.id ?? "unknown"); const features = packet.features ?? {};
const count = parseNumber(features.count, packet.members.length); const contract = String(features.option_contract_id ?? packet.id ?? "unknown");
const totalSize = parseNumber(features.total_size, 0); const count = parseNumber(features.count, packet.members.length);
const totalPremium = parseNumber(features.total_premium, 0); const totalSize = parseNumber(features.total_size, 0);
const notional = totalPremium * 100; const totalPremium = parseNumber(features.total_premium, 0);
const startTs = parseNumber(features.start_ts, packet.source_ts); const notional = totalPremium * 100;
const endTs = parseNumber(features.end_ts, startTs); const startTs = parseNumber(features.start_ts, packet.source_ts);
const windowMs = parseNumber(features.window_ms, 0); const endTs = parseNumber(features.end_ts, startTs);
const windowMs = parseNumber(features.window_ms, 0);
return ( return (
<div className="row" key={packet.id}> <div className="row" key={packet.id}>
<div> <div>
<div className="contract">{contract}</div> <div className="contract">{contract}</div>
<div className="meta flow-meta"> <div className="meta flow-meta">
<span>{formatFlowMetric(count)} prints</span> <span>{formatFlowMetric(count)} prints</span>
<span>{formatFlowMetric(totalSize)} size</span> <span>{formatFlowMetric(totalSize)} size</span>
<span>Premium ${formatPrice(totalPremium)}</span> <span>Premium ${formatPrice(totalPremium)}</span>
<span>Notional ${formatUsd(notional)}</span> <span>Notional ${formatUsd(notional)}</span>
{windowMs > 0 ? ( {windowMs > 0 ? (
<span>{formatFlowMetric(windowMs, "ms")}</span> <span>{formatFlowMetric(windowMs, "ms")}</span>
) : null} ) : null}
</div>
</div>
<div className="time">
{formatTime(startTs)} {formatTime(endTs)}
</div> </div>
</div> </div>
<div className="time"> );
{formatTime(startTs)} {formatTime(endTs)} })
</div> )}
</div> </div>
);
})
)}
</div> </div>
</section> </section>
@ -1694,50 +1715,51 @@ export default function HomePage() {
/> />
</div> </div>
<AlertSeverityStrip alerts={filteredAlerts} /> <div className="card-body">
<AlertSeverityStrip alerts={filteredAlerts} />
<div className="list" ref={alertsScroll.listRef}>
{mode !== "live" ? (
<div className="empty">Alerts are live-only in this build.</div>
) : filteredAlerts.length === 0 ? (
<div className="empty">
{tickerSet.size > 0
? "No alerts match the current filter."
: "No alerts yet. Start compute."}
</div>
) : (
filteredAlerts.map((alert) => {
const primary = alert.hits[0];
const direction = primary ? normalizeDirection(primary.direction) : "neutral";
<div className="list" ref={alertsScroll.listRef}> return (
{mode !== "live" ? ( <button
<div className="empty">Alerts are live-only in this build.</div> className="row row-button"
) : filteredAlerts.length === 0 ? ( key={`${alert.trace_id}-${alert.seq}`}
<div className="empty"> type="button"
{tickerSet.size > 0 onClick={() => setSelectedAlert(alert)}
? "No alerts match the current filter." >
: "No alerts yet. Start compute."} <div>
</div> <div className="contract">
) : ( {primary ? humanizeClassifierId(primary.classifier_id) : "Alert"}
filteredAlerts.map((alert) => { </div>
const primary = alert.hits[0]; <div className="meta">
const direction = primary ? normalizeDirection(primary.direction) : "neutral"; <span className={`pill severity-${alert.severity}`}>{alert.severity}</span>
<span>Score {Math.round(alert.score)}</span>
return ( <span>{alert.hits.length} hits</span>
<button {primary ? (
className="row row-button" <span className={`pill direction-${direction}`}>{direction}</span>
key={`${alert.trace_id}-${alert.seq}`} ) : null}
type="button" </div>
onClick={() => setSelectedAlert(alert)} {primary?.explanations?.[0] ? (
> <div className="note">{primary.explanations[0]}</div>
<div>
<div className="contract">
{primary ? humanizeClassifierId(primary.classifier_id) : "Alert"}
</div>
<div className="meta">
<span className={`pill severity-${alert.severity}`}>{alert.severity}</span>
<span>Score {Math.round(alert.score)}</span>
<span>{alert.hits.length} hits</span>
{primary ? (
<span className={`pill direction-${direction}`}>{direction}</span>
) : null} ) : null}
</div> </div>
{primary?.explanations?.[0] ? ( <div className="time">{formatTime(alert.source_ts)}</div>
<div className="note">{primary.explanations[0]}</div> </button>
) : null} );
</div> })
<div className="time">{formatTime(alert.source_ts)}</div> )}
</button> </div>
);
})
)}
</div> </div>
</section> </section>
@ -1766,35 +1788,37 @@ export default function HomePage() {
/> />
</div> </div>
<div className="list" ref={classifierScroll.listRef}> <div className="card-body">
{mode !== "live" ? ( <div className="list" ref={classifierScroll.listRef}>
<div className="empty">Classifier hits are live-only in this build.</div> {mode !== "live" ? (
) : filteredClassifierHits.length === 0 ? ( <div className="empty">Classifier hits are live-only in this build.</div>
<div className="empty"> ) : filteredClassifierHits.length === 0 ? (
{tickerSet.size > 0 <div className="empty">
? "No classifier hits match the current filter." {tickerSet.size > 0
: "No classifier hits yet. Start compute."} ? "No classifier hits match the current filter."
</div> : "No classifier hits yet. Start compute."}
) : ( </div>
filteredClassifierHits.map((hit) => { ) : (
const direction = normalizeDirection(hit.direction); filteredClassifierHits.map((hit) => {
return ( const direction = normalizeDirection(hit.direction);
<div className="row" key={`${hit.trace_id}-${hit.seq}`}> return (
<div> <div className="row" key={`${hit.trace_id}-${hit.seq}`}>
<div className="contract">{humanizeClassifierId(hit.classifier_id)}</div> <div>
<div className="meta"> <div className="contract">{humanizeClassifierId(hit.classifier_id)}</div>
<span className={`pill direction-${direction}`}>{direction}</span> <div className="meta">
<span>Confidence {formatConfidence(hit.confidence)}</span> <span className={`pill direction-${direction}`}>{direction}</span>
<span>Confidence {formatConfidence(hit.confidence)}</span>
</div>
{hit.explanations?.[0] ? (
<div className="note">{hit.explanations[0]}</div>
) : null}
</div> </div>
{hit.explanations?.[0] ? ( <div className="time">{formatTime(hit.source_ts)}</div>
<div className="note">{hit.explanations[0]}</div>
) : null}
</div> </div>
<div className="time">{formatTime(hit.source_ts)}</div> );
</div> })
); )}
}) </div>
)}
</div> </div>
</section> </section>
</div> </div>

View file

@ -60,10 +60,10 @@ const envSchema = z.object({
return value; return value;
}, z.boolean()) }, z.boolean())
.default(false), .default(false),
CLASSIFIER_SWEEP_MIN_PREMIUM: z.coerce.number().positive().default(50_000), CLASSIFIER_SWEEP_MIN_PREMIUM: z.coerce.number().positive().default(40_000),
CLASSIFIER_SWEEP_MIN_COUNT: z.coerce.number().int().positive().default(3), CLASSIFIER_SWEEP_MIN_COUNT: z.coerce.number().int().positive().default(3),
CLASSIFIER_SPIKE_MIN_PREMIUM: z.coerce.number().positive().default(25_000), CLASSIFIER_SPIKE_MIN_PREMIUM: z.coerce.number().positive().default(20_000),
CLASSIFIER_SPIKE_MIN_SIZE: z.coerce.number().int().positive().default(500) CLASSIFIER_SPIKE_MIN_SIZE: z.coerce.number().int().positive().default(400)
}); });
const env = readEnv(envSchema); const env = readEnv(envSchema);

View file

@ -11,18 +11,102 @@ type Burst = {
baseSize: number; baseSize: number;
exchange: string; exchange: string;
conditions?: string[]; conditions?: string[];
burstSize: number; printCount: number;
priceStep: number;
}; };
const MS_PER_DAY = 24 * 60 * 60 * 1000; const MS_PER_DAY = 24 * 60 * 60 * 1000;
const EXPIRY_OFFSETS = [7, 14, 28, 45, 60, 90]; const EXPIRY_OFFSETS = [0, 1, 7, 14, 28, 45, 60, 90];
const EXCHANGES = ["CBOE", "PHLX", "ISE", "ARCA", "BOX", "MIAX"]; const EXCHANGES = ["CBOE", "PHLX", "ISE", "ARCA", "BOX", "MIAX"];
const CONDITIONS = ["SWEEP", "ISO", "FILL", "TEST"]; const CONDITIONS = ["SWEEP", "ISO", "FILL", "TEST"];
const BURST_RUN_RANGE: [number, number] = [2, 4];
type Scenario = {
id: string;
weight: number;
right: "C" | "P" | "either";
countRange: [number, number];
sizeRange: [number, number];
premiumRange: [number, number];
priceTrend: "up" | "down" | "flat";
conditions?: string[];
};
const SCENARIOS: Scenario[] = [
{
id: "bullish_sweep",
weight: 35,
right: "C",
countRange: [7, 10],
sizeRange: [600, 1800],
premiumRange: [120_000, 240_000],
priceTrend: "up",
conditions: ["SWEEP"]
},
{
id: "bearish_sweep",
weight: 35,
right: "P",
countRange: [7, 10],
sizeRange: [600, 1800],
premiumRange: [120_000, 240_000],
priceTrend: "up",
conditions: ["SWEEP"]
},
{
id: "contract_spike",
weight: 20,
right: "either",
countRange: [5, 8],
sizeRange: [1200, 3200],
premiumRange: [60_000, 140_000],
priceTrend: "flat",
conditions: ["ISO"]
},
{
id: "noise",
weight: 10,
right: "either",
countRange: [2, 4],
sizeRange: [10, 200],
premiumRange: [500, 5000],
priceTrend: "flat",
conditions: ["FILL"]
}
];
const pick = <T,>(items: T[], seed: number): T => { const pick = <T,>(items: T[], seed: number): T => {
return items[Math.abs(seed) % items.length]; return items[Math.abs(seed) % items.length];
}; };
const pickInt = (min: number, max: number, seed: number): number => {
if (max <= min) {
return min;
}
const span = max - min + 1;
return min + (Math.abs(seed) % span);
};
const pickFloat = (min: number, max: number, seed: number): number => {
if (max <= min) {
return min;
}
const offset = (Math.abs(seed) % 1000) / 1000;
return min + (max - min) * offset;
};
const pickWeighted = <T extends { weight: number }>(items: T[], seed: number): T => {
const totalWeight = items.reduce((sum, item) => sum + item.weight, 0);
let target = Math.abs(seed) % totalWeight;
for (const item of items) {
if (target < item.weight) {
return item;
}
target -= item.weight;
}
return items[0];
};
const hashSymbol = (value: string): number => { const hashSymbol = (value: string): number => {
let hash = 0; let hash = 0;
for (let i = 0; i < value.length; i += 1) { for (let i = 0; i < value.length; i += 1) {
@ -44,21 +128,36 @@ const formatExpiry = (now: number, offsetDays: number): string => {
const buildBurst = (burstIndex: number, now: number): Burst => { const buildBurst = (burstIndex: number, now: number): Burst => {
const symbol = SP500_SYMBOLS[burstIndex % SP500_SYMBOLS.length]; const symbol = SP500_SYMBOLS[burstIndex % SP500_SYMBOLS.length];
const symbolHash = hashSymbol(symbol); const symbolHash = hashSymbol(symbol);
const basePrice = 30 + (symbolHash % 470); const scenario = pickWeighted(SCENARIOS, symbolHash + burstIndex * 7);
const baseUnderlying = 30 + (symbolHash % 470);
const expiryOffset = pick(EXPIRY_OFFSETS, symbolHash + burstIndex); const expiryOffset = pick(EXPIRY_OFFSETS, symbolHash + burstIndex);
const expiry = formatExpiry(now, expiryOffset); const expiry = formatExpiry(now, expiryOffset);
const strikeStep = basePrice >= 200 ? 10 : 5; const strikeStep = baseUnderlying >= 200 ? 10 : baseUnderlying >= 100 ? 5 : 2.5;
const strikeOffset = ((burstIndex % 7) - 3) * strikeStep; const moneynessSteps = scenario.id === "noise" ? 5 : 2;
const strike = Math.max(1, Math.round(basePrice / strikeStep) * strikeStep + strikeOffset); const strikeOffset = pickInt(-moneynessSteps, moneynessSteps, symbolHash + burstIndex * 11);
const right = burstIndex % 2 === 0 ? "C" : "P"; const strike = Math.max(
1,
Math.round(baseUnderlying / strikeStep) * strikeStep + strikeOffset * strikeStep
);
const right =
scenario.right === "either"
? (symbolHash + burstIndex) % 2 === 0
? "C"
: "P"
: scenario.right;
const contractId = `${symbol}-${expiry}-${formatStrike(strike)}-${right}`; const contractId = `${symbol}-${expiry}-${formatStrike(strike)}-${right}`;
const exchange = pick(EXCHANGES, burstIndex + symbolHash); const exchange = pick(EXCHANGES, burstIndex + symbolHash);
const isBlock = burstIndex % 4 === 0; const printCount = pickInt(scenario.countRange[0], scenario.countRange[1], symbolHash + burstIndex * 13);
const burstSize = isBlock ? 4 : burstIndex % 3 === 0 ? 2 : 1; const baseSize = pickInt(scenario.sizeRange[0], scenario.sizeRange[1], symbolHash + burstIndex * 17);
const baseSize = isBlock ? 1200 + (symbolHash % 1800) : 5 + (symbolHash % 180); const premiumTarget = pickFloat(
const distance = Math.abs(strike - basePrice); scenario.premiumRange[0],
const basePricePer = isBlock ? 12 + distance / strikeStep : 0.5 + distance / 30; scenario.premiumRange[1],
const conditions = isBlock ? [pick(CONDITIONS, burstIndex)] : undefined; symbolHash + burstIndex * 19
);
const basePricePer = Math.max(0.05, Number((premiumTarget / (baseSize * printCount)).toFixed(2)));
const conditions = scenario.conditions?.length ? scenario.conditions : [pick(CONDITIONS, burstIndex)];
const priceStep =
scenario.priceTrend === "up" ? 0.01 : scenario.priceTrend === "down" ? -0.01 : 0;
return { return {
contractId, contractId,
@ -66,7 +165,8 @@ const buildBurst = (burstIndex: number, now: number): Burst => {
baseSize, baseSize,
exchange, exchange,
conditions, conditions,
burstSize printCount,
priceStep
}; };
}; };
@ -79,7 +179,7 @@ export const createSyntheticOptionsAdapter = (
let seq = 0; let seq = 0;
let burstIndex = 0; let burstIndex = 0;
let currentBurst: Burst | null = null; let currentBurst: Burst | null = null;
let remaining = 0; let remainingRuns = 0;
let timer: ReturnType<typeof setInterval> | null = null; let timer: ReturnType<typeof setInterval> | null = null;
let stopped = false; let stopped = false;
@ -89,19 +189,20 @@ export const createSyntheticOptionsAdapter = (
} }
const now = Date.now(); const now = Date.now();
if (!currentBurst || remaining <= 0) { if (!currentBurst || remainingRuns <= 0) {
burstIndex += 1; burstIndex += 1;
currentBurst = buildBurst(burstIndex, now); currentBurst = buildBurst(burstIndex, now);
remaining = currentBurst.burstSize; remainingRuns = pickInt(BURST_RUN_RANGE[0], BURST_RUN_RANGE[1], burstIndex * 23);
} }
const burst = currentBurst; const burst = currentBurst;
const printsToEmit = remaining; const printsToEmit = burst.printCount;
for (let i = 0; i < printsToEmit; i += 1) { for (let i = 0; i < printsToEmit; i += 1) {
seq += 1; seq += 1;
const priceJitter = (i % 3) - 1; const priceJitter = ((i % 3) - 1) * 0.004;
const sizeJitter = (i % 4) - 1; const sizeJitter = ((i % 3) - 1) * 0.08;
const priceMultiplier = 1 + burst.priceStep * i + priceJitter;
const print: OptionPrint = { const print: OptionPrint = {
source_ts: now + i * 5, source_ts: now + i * 5,
ingest_ts: now + i * 5, ingest_ts: now + i * 5,
@ -109,8 +210,8 @@ export const createSyntheticOptionsAdapter = (
trace_id: `synthetic-options-${seq}`, trace_id: `synthetic-options-${seq}`,
ts: now + i * 5, ts: now + i * 5,
option_contract_id: burst.contractId, option_contract_id: burst.contractId,
price: Math.max(0.05, Number((burst.basePrice * (1 + priceJitter * 0.02)).toFixed(2))), price: Math.max(0.05, Number((burst.basePrice * priceMultiplier).toFixed(2))),
size: Math.max(1, Math.round(burst.baseSize * (1 + sizeJitter * 0.05))), size: Math.max(1, Math.round(burst.baseSize * (1 + sizeJitter))),
exchange: burst.exchange, exchange: burst.exchange,
conditions: burst.conditions conditions: burst.conditions
}; };
@ -118,7 +219,7 @@ export const createSyntheticOptionsAdapter = (
void handlers.onTrade(print); void handlers.onTrade(print);
} }
remaining = 0; remainingRuns -= 1;
}; };
timer = setInterval(emit, config.emitIntervalMs); timer = setInterval(emit, config.emitIntervalMs);