Tune synthetic flow mix and stabilize second-row card lists
This commit is contained in:
parent
eda219852f
commit
15fce370ef
4 changed files with 290 additions and 165 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue