Big refactoring
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user