Big refactoring
Deploy / lint (push) Failing after 21s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-03-03 15:20:18 +01:00
parent 181cfe6588
commit 0a011d4ce9
55 changed files with 6898 additions and 544 deletions
+26 -84
View File
@@ -1,15 +1,14 @@
import { useMemo, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useTickerDetail } from '../hooks/useTickerDetail';
import { useFetchSymbolData } from '../hooks/useFetchSymbolData';
import { CandlestickChart } from '../components/charts/CandlestickChart';
import { ScoreCard } from '../components/ui/ScoreCard';
import { SkeletonCard } from '../components/ui/Skeleton';
import { SentimentPanel } from '../components/ticker/SentimentPanel';
import { FundamentalsPanel } from '../components/ticker/FundamentalsPanel';
import { IndicatorSelector } from '../components/ticker/IndicatorSelector';
import { useToast } from '../components/ui/Toast';
import { fetchData } from '../api/ingestion';
import { RecommendationPanel } from '../components/ticker/RecommendationPanel';
import { formatPrice } from '../lib/format';
import type { TradeSetup } from '../lib/types';
@@ -67,43 +66,7 @@ function DataFreshnessBar({ items }: { items: DataStatusItem[] }) {
export default function TickerDetailPage() {
const { symbol = '' } = useParams<{ symbol: string }>();
const { ohlcv, scores, srLevels, sentiment, fundamentals, trades } = useTickerDetail(symbol);
const queryClient = useQueryClient();
const { addToast } = useToast();
const ingestion = useMutation({
mutationFn: () => fetchData(symbol),
onSuccess: (result: any) => {
// Show per-source status breakdown
const sources = result?.sources;
if (sources) {
const parts: string[] = [];
for (const [name, info] of Object.entries(sources) as [string, any][]) {
const label = name.charAt(0).toUpperCase() + name.slice(1);
if (info.status === 'ok') {
parts.push(`${label}`);
} else if (info.status === 'skipped') {
parts.push(`${label}: skipped (${info.message})`);
} else {
parts.push(`${label} ✗: ${info.message}`);
}
}
const hasError = Object.values(sources).some((s: any) => s.status === 'error');
const hasSkip = Object.values(sources).some((s: any) => s.status === 'skipped');
const toastType = hasError ? 'error' : hasSkip ? 'info' : 'success';
addToast(toastType, parts.join(' · '));
} else {
addToast('success', `Data fetched for ${symbol.toUpperCase()}`);
}
queryClient.invalidateQueries({ queryKey: ['ohlcv', symbol] });
queryClient.invalidateQueries({ queryKey: ['sentiment', symbol] });
queryClient.invalidateQueries({ queryKey: ['fundamentals', symbol] });
queryClient.invalidateQueries({ queryKey: ['sr-levels', symbol] });
queryClient.invalidateQueries({ queryKey: ['scores', symbol] });
},
onError: (err: Error) => {
addToast('error', err.message || 'Failed to fetch data');
},
});
const ingestion = useFetchSymbolData();
const dataStatus: DataStatusItem[] = useMemo(() => [
{
@@ -140,18 +103,28 @@ export default function TickerDetailPage() {
}
}, [trades.error]);
// Pick the latest trade setup for the current symbol
const tradeSetup: TradeSetup | undefined = useMemo(() => {
if (trades.error || !trades.data) return undefined;
const matching = trades.data.filter(
(t) => t.symbol.toUpperCase() === symbol.toUpperCase(),
);
if (matching.length === 0) return undefined;
return matching.reduce((latest, t) =>
new Date(t.detected_at) > new Date(latest.detected_at) ? t : latest,
);
const setupsForSymbol: TradeSetup[] = useMemo(() => {
if (trades.error || !trades.data) return [];
return trades.data.filter((t) => t.symbol.toUpperCase() === symbol.toUpperCase());
}, [trades.data, trades.error, symbol]);
const longSetup = useMemo(
() => setupsForSymbol?.find((s) => s.direction === 'long'),
[setupsForSymbol],
);
const shortSetup = useMemo(
() => setupsForSymbol?.find((s) => s.direction === 'short'),
[setupsForSymbol],
);
// Use the highest-confidence setup for chart overlay fallback.
const tradeSetup: TradeSetup | undefined = useMemo(() => {
const candidates = [longSetup, shortSetup].filter(Boolean) as TradeSetup[];
if (candidates.length === 0) return undefined;
return candidates.sort((a, b) => (b.confidence_score ?? 0) - (a.confidence_score ?? 0))[0];
}, [longSetup, shortSetup]);
// Sort visible S/R levels by strength for the table (only levels within chart zones)
const sortedLevels = useMemo(() => {
if (!srLevels.data?.visible_levels) return [];
@@ -167,7 +140,7 @@ export default function TickerDetailPage() {
<p className="text-sm text-gray-500 mt-0.5">Ticker Detail</p>
</div>
<button
onClick={() => ingestion.mutate()}
onClick={() => ingestion.mutate(symbol)}
disabled={ingestion.isPending}
className="btn-gradient inline-flex items-center gap-2 px-5 py-2.5 text-sm disabled:opacity-60 disabled:cursor-not-allowed"
>
@@ -184,6 +157,8 @@ export default function TickerDetailPage() {
{/* Data freshness bar */}
<DataFreshnessBar items={dataStatus} />
<RecommendationPanel symbol={symbol} longSetup={longSetup} shortSetup={shortSetup} />
{/* Chart Section */}
<section>
<h2 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Price Chart</h2>
@@ -204,39 +179,6 @@ export default function TickerDetailPage() {
)}
</section>
{/* Trade Setup Summary Card */}
{tradeSetup && (
<section>
<h2 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Trade Setup</h2>
<div className="glass p-5">
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Direction</span>
<span className={`text-sm font-semibold ${tradeSetup.direction === 'long' ? 'text-emerald-400' : 'text-red-400'}`}>
{tradeSetup.direction.toUpperCase()}
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Entry</span>
<span className="text-sm font-mono text-blue-300">{formatPrice(tradeSetup.entry_price)}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Stop</span>
<span className="text-sm font-mono text-red-400">{formatPrice(tradeSetup.stop_loss)}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Target</span>
<span className="text-sm font-mono text-emerald-400">{formatPrice(tradeSetup.target)}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500">R:R</span>
<span className="text-sm font-semibold text-gray-200">{tradeSetup.rr_ratio.toFixed(2)}</span>
</div>
</div>
</div>
</section>
)}
{/* Scores + Side Panels */}
<div className="grid gap-6 lg:grid-cols-3">
<section>