Blum / frontend /components /ChartAnalystPanel.tsx
Italianhype's picture
Fix narratives and add financial analyst chat
b7597c9 verified
Raw
History Blame Contribute Delete
11.6 kB
"use client";
import { useEffect, useMemo, useState } from "react";
import { api } from "@/lib/api";
import { ChartReport, PricePoint } from "@/lib/types";
import { PlotPanel } from "./PlotPanel";
const TIMEFRAMES = ["1D", "5D", "1M", "3M", "6M", "YTD", "1Y", "5Y"];
export function ChartAnalystPanel({ ticker, prices = [] }: { ticker: string; prices?: PricePoint[] }) {
const [timeframe, setTimeframe] = useState("6M");
const [report, setReport] = useState<ChartReport | null>(null);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const loadReport = async (tf = timeframe) => {
setError("");
try {
setReport(await api.chartTechnicalReport(ticker, tf));
} catch (err) {
setError((err as Error).message);
}
};
useEffect(() => {
loadReport(timeframe);
}, [ticker, timeframe]);
const analyze = async () => {
setBusy(true);
setError("");
try {
setReport(await api.chartAnalyzeTicker(ticker, timeframe, periodFor(timeframe), false));
} catch (err) {
setError((err as Error).message);
} finally {
setBusy(false);
}
};
const chartPrices = useMemo(() => trimPrices(report?.price_series?.length ? report.price_series : prices, timeframe), [report, prices, timeframe]);
const hybrid = (report?.hybrid_analysis ?? {}) as ChartReport["hybrid_analysis"];
const deterministic = (report?.deterministic_analysis ?? {}) as Record<string, any>;
const levels = hybrid.key_levels ?? {};
const visual = report?.visual_analysis ?? {};
const plot = chartPlot(chartPrices, levels);
return (
<section className="chart-analyst-shell">
<div className="page-header compact-header">
<div>
<div className="kicker">Chart Vision Technical Analyst</div>
<h2>{ticker} technical chart intelligence.</h2>
</div>
<div className="header-actions">
<select className="input" value={timeframe} onChange={(event) => setTimeframe(event.target.value)}>
{TIMEFRAMES.map((item) => <option key={item}>{item}</option>)}
</select>
<button className="button primary" onClick={analyze} disabled={busy}>{busy ? "Analyzing..." : "Analyze chart"}</button>
</div>
</div>
{error && <div className="empty-state" style={{ marginBottom: 12 }}>Chart API warning: {error}</div>}
<section className="grid-2">
<PlotPanel
title="Price Structure With Key Levels"
data={plot.data}
layout={plot.layout}
height={430}
emptyMessage="Waiting for stored OHLCV data to render the chart."
/>
<TechnicalSnapshot report={report} />
</section>
<section className="grid-2" style={{ marginTop: 12 }}>
<KeyLevels levels={levels} />
<SignalEvidence hybrid={hybrid} />
</section>
<section className="grid-2" style={{ marginTop: 12 }}>
<AnalystReport hybrid={hybrid} visual={visual} deterministic={deterministic} />
<HistoricalSimilarity similarity={hybrid.historical_similarity ?? {}} />
</section>
</section>
);
}
function TechnicalSnapshot({ report }: { report: ChartReport | null }) {
const d = report?.deterministic_analysis ?? {};
const h = (report?.hybrid_analysis ?? {}) as ChartReport["hybrid_analysis"];
const momentum = d.momentum ?? {};
const volatility = d.volatility ?? {};
const volume = d.volume ?? {};
const pattern = d.patterns?.[0]?.pattern ?? "pending";
return (
<div className="panel chart-snapshot-panel">
<div className="panel-head">
<span>Technical Snapshot</span>
<strong>{Number(h.confidence_score ?? report?.confidence ?? 0).toFixed(1)} confidence</strong>
</div>
<div className="brain-status-grid">
<SnapshotMetric label="Trend" value={String(d.trend_direction ?? "pending").replaceAll("_", " ")} />
<SnapshotMetric label="Momentum" value={String(momentum.state ?? "pending").replaceAll("_", " ")} />
<SnapshotMetric label="Volatility" value={String(volatility.regime ?? "pending").replaceAll("_", " ")} />
<SnapshotMetric label="Volume" value={String(volume.volume_state ?? "pending").replaceAll("_", " ")} />
<SnapshotMetric label="Pattern" value={String(pattern).replaceAll("_", " ")} />
<SnapshotMetric label="Breakout" value={`${Number(d.breakout_probability?.score ?? 0).toFixed(1)}/100`} />
</div>
<div className="brain-drift info">
<strong>{String(h.timeframe_relevance ?? "timeframe pending").replaceAll("_", " ")}</strong>
<span>{h.trend_summary ?? "Technical summary will appear when stored OHLCV evidence is sufficient for the autonomous chart analyst."}</span>
</div>
<p>{(report?.warnings ?? h.warnings)?.join(" ") ?? "Vision model status and deterministic analysis warnings will appear here."}</p>
</div>
);
}
function KeyLevels({ levels }: { levels: Record<string, any> }) {
return (
<div className="panel">
<div className="panel-head"><span>Key Levels</span><strong>zones, not guarantees</strong></div>
<div className="key-level-grid">
<Level label="Support 1" value={levels.support_1} />
<Level label="Support 2" value={levels.support_2} />
<Level label="Resistance 1" value={levels.resistance_1} />
<Level label="Resistance 2" value={levels.resistance_2} />
<Level label="Breakout" value={levels.breakout_level} />
<Level label="Breakdown" value={levels.breakdown_level} />
<Level label="Invalidation" value={levels.invalidation_level} />
</div>
</div>
);
}
function SignalEvidence({ hybrid }: { hybrid: any }) {
return (
<div className="panel">
<div className="panel-head"><span>Signal Evidence</span><strong>{hybrid.technical_signals?.length ?? 0} signals</strong></div>
<div className="evidence-columns">
<EvidenceList title="Bullish Evidence" rows={hybrid.bullish_evidence ?? []} />
<EvidenceList title="Bearish Evidence" rows={hybrid.bearish_evidence ?? []} />
<EvidenceList title="Neutral / Uncertain" rows={hybrid.neutral_evidence ?? []} />
</div>
</div>
);
}
function AnalystReport({ hybrid, visual, deterministic }: { hybrid: any; visual: Record<string, any>; deterministic: Record<string, any> }) {
return (
<div className="panel">
<div className="panel-head">
<span>Analyst Report</span>
<strong>{String(visual.mode ?? "deterministic").replaceAll("_", " ")}</strong>
</div>
<p>{hybrid.analyst_report ?? deterministic.technical_summary ?? "The autonomous chart analyst is waiting for sufficient deterministic technical evidence."}</p>
<div className="scenario-list">
{(hybrid.possible_scenarios ?? []).map((scenario: any) => (
<div key={scenario.name}>
<strong>{scenario.name}</strong>
<span>{scenario.condition}</span>
<span>{scenario.impact}</span>
</div>
))}
</div>
<div className="learning-event-list" style={{ marginTop: 10 }}>
{(hybrid.what_to_watch_next ?? []).slice(0, 6).map((item: string) => <span key={item}>{item}</span>)}
</div>
</div>
);
}
function HistoricalSimilarity({ similarity }: { similarity: Record<string, any> }) {
return (
<div className="panel">
<div className="panel-head">
<span>Historical Similarity</span>
<strong>{similarity.reliability_score ? `${Number(similarity.reliability_score).toFixed(1)} reliability` : "maturing"}</strong>
</div>
<div className="brain-status-grid">
<SnapshotMetric label="Similar Setups" value={similarity.similar_chart_setups ?? 0} />
<SnapshotMetric label="Mature Cases" value={similarity.mature_cases ?? 0} />
<SnapshotMetric label="7D Avg Return" value={formatPercent(similarity.average_forward_return_7d)} />
<SnapshotMetric label="Success Rate" value={ratio(similarity.success_rate)} />
</div>
<p>{similarity.explanation ?? "Chart pattern memory will mature as future OHLCV outcomes are observed."}</p>
</div>
);
}
function SnapshotMetric({ label, value }: { label: string; value: string | number }) {
return <div className="brain-metric"><span>{label}</span><strong>{value}</strong></div>;
}
function Level({ label, value }: { label: string; value: any }) {
return <div><span>{label}</span><strong>{formatLevel(value)}</strong></div>;
}
function EvidenceList({ title, rows }: { title: string; rows: string[] }) {
return (
<div>
<h3>{title}</h3>
<div className="learning-event-list">
{rows.length ? rows.slice(0, 6).map((item) => <span key={item}>{item}</span>) : <span>No confirmed evidence yet.</span>}
</div>
</div>
);
}
function chartPlot(prices: PricePoint[], levels: Record<string, any>) {
const x = prices.map((point) => point.date);
const shapes = levelShapes(levels, x);
return {
data: [
{
type: "candlestick",
x,
open: prices.map((point) => point.open ?? point.close),
high: prices.map((point) => point.high ?? point.close),
low: prices.map((point) => point.low ?? point.close),
close: prices.map((point) => point.close),
increasing: { line: { color: "#20e070" } },
decreasing: { line: { color: "#ff4d5e" } },
name: "OHLC"
},
{
type: "bar",
x,
y: prices.map((point) => point.volume ?? 0),
yaxis: "y2",
marker: { color: "rgba(77,216,255,.24)" },
name: "Volume"
}
],
layout: {
shapes,
xaxis: { rangeslider: { visible: false }, gridcolor: "rgba(255,255,255,.06)" },
yaxis: { domain: [0.24, 1], gridcolor: "rgba(255,255,255,.06)" },
yaxis2: { domain: [0, 0.17], gridcolor: "rgba(255,255,255,.04)" },
showlegend: false
}
};
}
function levelShapes(levels: Record<string, any>, x: string[]) {
if (!x.length) return [];
const x0 = x[0];
const x1 = x[x.length - 1];
const candidates = [
{ value: levels.support_1, color: "#20e070" },
{ value: levels.support_2, color: "#20e070" },
{ value: levels.resistance_1, color: "#ff4d5e" },
{ value: levels.resistance_2, color: "#ff4d5e" },
{ value: levels.invalidation_level, color: "#ffb000" }
];
return candidates.filter((item) => item.value).map((item) => ({
type: "line",
xref: "x",
yref: "y",
x0,
x1,
y0: Number(item.value),
y1: Number(item.value),
line: { color: item.color, width: 1, dash: "dot" }
}));
}
function trimPrices(prices: PricePoint[], timeframe: string) {
const limits: Record<string, number> = { "1D": 96, "5D": 120, "1M": 42, "3M": 84, "6M": 150, "YTD": 260, "1Y": 260, "5Y": 1260 };
return prices.slice(-(limits[timeframe] ?? 150));
}
function periodFor(timeframe: string) {
const periods: Record<string, string> = { "1D": "5d", "5D": "1mo", "1M": "3mo", "3M": "6mo", "6M": "1y", "YTD": "1y", "1Y": "2y", "5Y": "5y" };
return periods[timeframe] ?? "1y";
}
function formatLevel(value: any) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return "n/a";
return Number(value).toFixed(2);
}
function formatPercent(value: any) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return "Pending";
return `${Number(value).toFixed(2)}%`;
}
function ratio(value: any) {
if (value === null || value === undefined || Number.isNaN(Number(value))) return "Pending";
return `${Math.round(Number(value) * 100)}%`;
}