gammanexus/client/src/pages/gamma-levels.tsx

175 lines
6.1 KiB
TypeScript

import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import type { GexProfile, Summary } from "@shared/schema";
import { useSymbol } from "@/lib/symbol-context";
import { fmtCompactCurrency, fmtStrike, fmtPct } from "@/lib/format";
import { GexChart } from "@/components/gex-chart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { MetricTooltip } from "@/components/metric-tooltip";
import { LoadingBlock } from "@/components/loading-block";
import { cn } from "@/lib/utils";
export default function GammaLevelsPage() {
const { symbol } = useSymbol();
const summary = useQuery<Summary>({ queryKey: ["/api/market", symbol, "summary"] });
const gex = useQuery<GexProfile>({ queryKey: ["/api/market", symbol, "gex"] });
const s = summary.data;
const g = gex.data;
// Sort bars by absolute net gex magnitude — the top 10 are the meaningful walls.
const ranked = useMemo(() => {
if (!g) return [];
return [...g.bars]
.map((b) => ({
...b,
magnitude: Math.abs(b.netGex),
absCall: Math.abs(b.callGex),
absPut: Math.abs(b.putGex),
}))
.sort((a, b) => b.magnitude - a.magnitude)
.slice(0, 12);
}, [g]);
return (
<div className="flex flex-col gap-5 p-4 sm:p-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 className="text-base font-semibold tracking-tight">Gamma levels</h2>
<p className="mt-1 text-xs text-muted-foreground">
Dealer gamma by strike with the top concentration zones. Use this view to identify pin candidates and break levels.
</p>
</div>
{s && (
<div className="flex flex-wrap gap-2 text-xs">
<KeyLevel
color="hvl"
label="HVL"
value={fmtStrike(s.hvl)}
from={s.spot}
tooltipMetric="hvl"
/>
<KeyLevel
color="call-wall"
label="Call Wall"
value={fmtStrike(s.callWall)}
from={s.spot}
tooltipMetric="callWall"
/>
<KeyLevel
color="put-wall"
label="Put Wall"
value={fmtStrike(s.putWall)}
from={s.spot}
tooltipMetric="putWall"
/>
</div>
)}
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold">Gamma exposure profile</CardTitle>
</CardHeader>
<CardContent>
{g ? <GexChart profile={g} height={420} /> : <LoadingBlock height={420} />}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-semibold">Top strike concentrations</CardTitle>
<p className="mt-1 text-xs text-muted-foreground">
Strikes ranked by absolute net gamma. Positive net values indicate dealer-long gamma; negative values indicate dealer-short gamma.
</p>
</CardHeader>
<CardContent className="px-0 sm:px-2">
<div className="overflow-x-auto">
<Table data-testid="table-top-strikes">
<TableHeader className="sticky top-0 bg-card">
<TableRow>
<TableHead className="w-[110px]">Strike</TableHead>
<TableHead className="text-right">Call GEX</TableHead>
<TableHead className="text-right">Put GEX</TableHead>
<TableHead className="text-right">Net GEX</TableHead>
<TableHead className="text-right">Distance</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ranked.map((row) => {
const dist = s ? ((row.strike - s.spot) / s.spot) * 100 : 0;
return (
<TableRow key={row.strike} data-testid={`row-strike-${row.strike}`}>
<TableCell className="num font-medium">{fmtStrike(row.strike)}</TableCell>
<TableCell className="num text-right text-pos">
{fmtCompactCurrency(row.callGex)}
</TableCell>
<TableCell className="num text-right text-neg">
{fmtCompactCurrency(row.putGex)}
</TableCell>
<TableCell
className={cn(
"num text-right font-medium",
row.netGex >= 0 ? "text-pos" : "text-neg",
)}
>
{fmtCompactCurrency(row.netGex)}
</TableCell>
<TableCell className="num text-right text-muted-foreground">
{fmtPct(dist)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}
function KeyLevel({
color,
label,
value,
from,
tooltipMetric,
}: {
color: "hvl" | "call-wall" | "put-wall";
label: string;
value: string;
from: number;
tooltipMetric: "hvl" | "callWall" | "putWall";
}) {
const numericValue = Number(value.replace(/,/g, ""));
const distPct = ((numericValue - from) / from) * 100;
return (
<Badge
variant="outline"
className={cn(
"gap-2 border-border px-2.5 py-1 font-mono text-xs",
color === "hvl" && "bg-hvl/10 text-hvl",
color === "call-wall" && "bg-call-wall/10 text-call-wall",
color === "put-wall" && "bg-put-wall/10 text-put-wall",
)}
data-testid={`badge-${tooltipMetric}`}
>
<span className="font-sans font-medium uppercase tracking-wide opacity-80">{label}</span>
<span>{value}</span>
<span className="text-foreground/70">{fmtPct(distPct)}</span>
<MetricTooltip metric={tooltipMetric} className="h-3 w-3" />
</Badge>
);
}