feat: fase 3 awam — Screener preset, Alerts template, Routines manfaat, Portfolio saran
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

This commit is contained in:
asepharyana committed 2026-09-15 20:23:40 +07:00
1 parent 9fa47240dc
commit a35182ead0
4 files changed
+162 -256

No files matched your search

+35 -57
View File
@@ -1,87 +1,65 @@
import { createResource, For, Show } from "solid-js";
import { api } from "../lib/api";
import { Term, IstilahStrip } from "../components/Awam";
import { Bar } from "solid-chartjs";
import { Chart, registerables } from "chart.js";
import { PageHead } from "../components/ui";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
import { Badge } from "../components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { Skeleton } from "../components/ui/skeleton";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
Chart.register(...registerables);
export default function Portfolio() {
const [risk] = createResource(() => api.risk());
const [acc] = createResource(() => api.accuracy());
const conc = () => risk()?.concentration || [];
const betaN = () => Number(risk()?.beta ?? 1);
const betaArti = () => {
const b = betaN();
if (b > 1.2) return "bergerak lebih liar dari pasar — naik lebih kencang, turun lebih dalam.";
if (b < 0.8) return "lebih kalem dari pasar — cocok buat yang tidak suka deg-degan.";
return "geraknya seirama pasar — tidak terlalu liar, tidak terlalu kalem.";
};
const saran = () => {
const c = conc();
if (!c.length) return "";
const top = c[0];
if (top.weight > 0.4) return `${top.ticker} porsinya ${(top.weight * 100).toFixed(0)}% — kebanyakan telur di satu keranjang. Pertimbangkan tambah saham beda sektor.`;
const sek = [...new Set(c.map((x) => x.sector))];
if (sek.length <= 2) return `Cuma ${sek.length} sektor (${sek.join(", ")}) — kalau sektor itu jatuh, semua ikut. Coba lirik sektor lain.`;
return "Sebarannya sudah lumayan — tidak numpuk di satu tempat. Pertahankan. 👍";
};
const chartData = () => ({
labels: (risk()?.concentration || []).map((c) => c.ticker),
datasets: [{ label: "Weight %", data: (risk()?.concentration || []).map((c) => +(c.weight * 100).toFixed(1)) }],
labels: conc().map((c) => c.ticker),
datasets: [{ label: "Porsi %", data: conc().map((c) => +(c.weight * 100).toFixed(1)) }],
});
return (
<div class="space-y-4">
<PageHead title="Portfolio Risk + Accuracy Ledger" sub="Computed from your watchlist." />
<Show when={!risk.loading && !acc.loading} fallback={<div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div>}>
<PageHead title="Risiko portofolio kamu 💼" sub="Dihitung dari watchlist-mu. Bahasa manusia, bukan laporan bank." />
<Show when={!risk.loading} fallback={<div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div>}>
<Card class="border-primary/30 bg-gradient-to-br from-primary/10 to-transparent">
<CardHeader><CardTitle class="text-xl">Saran 1 kalimat 💡</CardTitle></CardHeader>
<CardContent><p class="text-base leading-relaxed">{saran() || "Tambah saham ke watchlist dulu biar bisa dinilai."}</p></CardContent>
</Card>
<div class="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader><CardTitle>Concentration</CardTitle></CardHeader>
<CardHeader><CardTitle>Porsi tiap saham</CardTitle><CardDescription>Siapa yang paling dominan di <Term kata="portofolio" />-mu.</CardDescription></CardHeader>
<CardContent>
<Show when={(risk()?.concentration || []).length} fallback={<p class="text-sm text-muted-foreground">Add tickers to your watchlist to see concentration.</p>}>
<Show when={conc().length} fallback={<p class="text-sm text-muted-foreground">Tambah saham ke watchlist dulu.</p>}>
<Bar data={chartData()} options={{ responsive: true }} />
</Show>
<For each={risk()?.warnings || []}>{(w) => <p class="text-sm">⚠️ {w}</p>}</For>
<For each={risk()?.warnings || []}>{(w) => <p class="mt-1 text-sm">⚠️ {w}</p>}</For>
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Beta vs index</CardTitle></CardHeader>
<CardHeader><CardTitle>Kelakuan vs pasar</CardTitle><CardDescription>Skor beta = {betaN().toFixed(2)}</CardDescription></CardHeader>
<CardContent class="space-y-3">
<p class="font-mono text-2xl font-bold">{risk() ? String(risk()!.beta) : "…"}</p>
<h3 class="text-sm font-semibold">Correlation heatmap</h3>
<CorrHeatmap matrix={risk()?.correlation || {}} />
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Agent accuracy</CardTitle></CardHeader>
<CardContent>
<Table>
<TableHeader><TableRow><TableHead>Agent</TableHead><TableHead class="text-right">Calls</TableHead><TableHead class="text-right">Resolved</TableHead><TableHead class="text-right">Hit %</TableHead></TableRow></TableHeader>
<TableBody>
<For each={acc()?.agents || []}>
{(a) => <TableRow><TableCell>{a.agent}</TableCell><TableCell class="text-right">{a.calls}</TableCell><TableCell class="text-right">{a.resolved}</TableCell><TableCell class="text-right">{(a.hit_rate * 100).toFixed(0)}%</TableCell></TableRow>}
</For>
</TableBody>
</Table>
<p class="text-base">Portofolio kamu <strong>{betaArti()}</strong></p>
<div><Badge variant="outline"><Term kata="diversifikasi" />: {[...new Set(conc().map((x) => x.sector))].length} sektor</Badge></div>
</CardContent>
</Card>
</div>
</Show>
<div><IstilahStrip /></div>
</div>
);
}
function CorrHeatmap(props: { matrix: Record<string, Record<string, number>> }) {
const keys = () => Object.keys(props.matrix);
const cell = (v: number) => {
const a = Math.min(1, Math.abs(v));
return v >= 0 ? `rgba(34,197,94,${(0.12 + 0.6 * a).toFixed(2)})` : `rgba(239,68,68,${(0.12 + 0.6 * a).toFixed(2)})`;
};
return (
<Show when={keys().length} fallback={<p class="text-sm text-muted-foreground">No correlation data yet.</p>}>
<div class="overflow-x-auto"><Table>
<TableHeader><TableRow><TableHead></TableHead><For each={keys()}>{(k) => <TableHead>{k}</TableHead>}</For></TableRow></TableHeader>
<TableBody>
<For each={keys()}>
{(r) => (
<TableRow>
<TableCell class="font-bold">{r}</TableCell>
<For each={keys()}>
{(c) => {
const v = props.matrix[r]?.[c] ?? 0;
return <TableCell style={{ background: cell(v) }} title={`${r}/${c} = ${v.toFixed(2)}`}>{v.toFixed(2)}</TableCell>;
}}
</For>
</TableRow>
)}
</For>
</TableBody>
</Table></div>
</Show>
);
}