first commit
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import { formatPercent, formatLargeNumber } from '../../lib/format';
|
||||
import type { FundamentalResponse } from '../../lib/types';
|
||||
|
||||
interface FundamentalsPanelProps {
|
||||
data: FundamentalResponse;
|
||||
}
|
||||
|
||||
export function FundamentalsPanel({ data }: FundamentalsPanelProps) {
|
||||
const items = [
|
||||
{ label: 'P/E Ratio', value: data.pe_ratio !== null ? data.pe_ratio.toFixed(2) : '—' },
|
||||
{ label: 'Revenue Growth', value: data.revenue_growth !== null ? formatPercent(data.revenue_growth) : '—' },
|
||||
{ label: 'Earnings Surprise', value: data.earnings_surprise !== null ? formatPercent(data.earnings_surprise) : '—' },
|
||||
{ label: 'Market Cap', value: data.market_cap !== null ? formatLargeNumber(data.market_cap) : '—' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="glass p-5">
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Fundamentals</h3>
|
||||
<div className="space-y-2.5 text-sm">
|
||||
{items.map((item) => (
|
||||
<div key={item.label} className="flex justify-between">
|
||||
<span className="text-gray-400">{item.label}</span>
|
||||
<span className="text-gray-200">{item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
{data.fetched_at && (
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
Updated {new Date(data.fetched_at).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getIndicator, getEMACross } from '../../api/indicators';
|
||||
import type { IndicatorResult, EMACrossResult } from '../../lib/types';
|
||||
|
||||
const INDICATOR_TYPES = ['ADX', 'EMA', 'RSI', 'ATR', 'volume_profile', 'pivot_points'] as const;
|
||||
|
||||
interface IndicatorSelectorProps {
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const signalColors: Record<string, string> = {
|
||||
bullish: 'text-emerald-400',
|
||||
bearish: 'text-red-400',
|
||||
neutral: 'text-gray-300',
|
||||
};
|
||||
|
||||
function IndicatorResultDisplay({ result }: { result: IndicatorResult }) {
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Type</span>
|
||||
<span className="text-gray-200">{result.indicator_type}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Normalized Score</span>
|
||||
<span className="text-gray-200">{result.score.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Bars Used</span>
|
||||
<span className="text-gray-200">{result.bars_used}</span>
|
||||
</div>
|
||||
{Object.keys(result.values).length > 0 && (
|
||||
<div className="mt-2 border-t border-white/[0.06] pt-2">
|
||||
<p className="mb-1 text-[10px] font-medium uppercase tracking-widest text-gray-500">Values</p>
|
||||
{Object.entries(result.values).map(([key, val]) => (
|
||||
<div key={key} className="flex justify-between">
|
||||
<span className="text-gray-400">{key}</span>
|
||||
<span className="text-gray-200">{typeof val === 'number' ? val.toFixed(4) : String(val)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EMACrossDisplay({ result }: { result: EMACrossResult }) {
|
||||
return (
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Signal</span>
|
||||
<span className={signalColors[result.signal] ?? 'text-gray-300'}>{result.signal}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Short EMA ({result.short_period})</span>
|
||||
<span className="text-gray-200">{result.short_ema.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Long EMA ({result.long_period})</span>
|
||||
<span className="text-gray-200">{result.long_ema.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function IndicatorSelector({ symbol }: IndicatorSelectorProps) {
|
||||
const [selectedType, setSelectedType] = useState<string>('');
|
||||
const [showEMACross, setShowEMACross] = useState(false);
|
||||
|
||||
const indicatorQuery = useQuery({
|
||||
queryKey: ['indicator', symbol, selectedType],
|
||||
queryFn: () => getIndicator(symbol, selectedType),
|
||||
enabled: !!symbol && !!selectedType,
|
||||
});
|
||||
|
||||
const emaCrossQuery = useQuery({
|
||||
queryKey: ['ema-cross', symbol],
|
||||
queryFn: () => getEMACross(symbol),
|
||||
enabled: !!symbol && showEMACross,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="glass p-5">
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Indicators</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
className="w-full input-glass px-3 py-2.5 text-sm"
|
||||
>
|
||||
<option value="">Select indicator…</option>
|
||||
{INDICATOR_TYPES.map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedType && indicatorQuery.isLoading && (
|
||||
<div className="animate-pulse space-y-2">
|
||||
<div className="h-4 w-3/4 rounded bg-white/[0.05]" />
|
||||
<div className="h-4 w-1/2 rounded bg-white/[0.05]" />
|
||||
<div className="h-4 w-2/3 rounded bg-white/[0.05]" />
|
||||
</div>
|
||||
)}
|
||||
{selectedType && indicatorQuery.isError && (
|
||||
<p className="text-sm text-red-400">
|
||||
{indicatorQuery.error instanceof Error ? indicatorQuery.error.message : 'Failed to load indicator'}
|
||||
</p>
|
||||
)}
|
||||
{indicatorQuery.data && <IndicatorResultDisplay result={indicatorQuery.data} />}
|
||||
|
||||
<div className="mt-4 border-t border-white/[0.06] pt-4">
|
||||
<button
|
||||
onClick={() => setShowEMACross(true)}
|
||||
disabled={showEMACross && emaCrossQuery.isLoading}
|
||||
className="w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2.5 text-sm text-gray-200 transition-all duration-200 hover:bg-white/[0.07] disabled:opacity-50"
|
||||
>
|
||||
{emaCrossQuery.isLoading ? 'Loading EMA Cross…' : 'Show EMA Cross Signal'}
|
||||
</button>
|
||||
{emaCrossQuery.isError && (
|
||||
<p className="mt-2 text-sm text-red-400">
|
||||
{emaCrossQuery.error instanceof Error ? emaCrossQuery.error.message : 'Failed to load EMA cross'}
|
||||
</p>
|
||||
)}
|
||||
{emaCrossQuery.data && (
|
||||
<div className="mt-3"><EMACrossDisplay result={emaCrossQuery.data} /></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ReferenceLine } from 'recharts';
|
||||
import type { SRLevel } from '../../lib/types';
|
||||
import { formatPrice } from '../../lib/format';
|
||||
|
||||
interface SROverlayProps {
|
||||
levels: SRLevel[];
|
||||
}
|
||||
|
||||
export function SROverlay({ levels }: SROverlayProps) {
|
||||
return (
|
||||
<>
|
||||
{levels.map((level) => {
|
||||
const isSupport = level.type === 'support';
|
||||
return (
|
||||
<ReferenceLine
|
||||
key={level.id}
|
||||
y={level.price_level}
|
||||
stroke={isSupport ? '#22c55e' : '#ef4444'}
|
||||
strokeDasharray="6 3"
|
||||
strokeWidth={1.5}
|
||||
label={{
|
||||
value: formatPrice(level.price_level),
|
||||
position: 'right',
|
||||
fill: isSupport ? '#22c55e' : '#ef4444',
|
||||
fontSize: 11,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { formatPercent } from '../../lib/format';
|
||||
import type { SentimentResponse } from '../../lib/types';
|
||||
|
||||
interface SentimentPanelProps {
|
||||
data: SentimentResponse;
|
||||
}
|
||||
|
||||
const classificationColors: Record<string, string> = {
|
||||
bullish: 'text-emerald-400',
|
||||
bearish: 'text-red-400',
|
||||
neutral: 'text-gray-300',
|
||||
};
|
||||
|
||||
export function SentimentPanel({ data }: SentimentPanelProps) {
|
||||
const latest = data.scores[0];
|
||||
|
||||
return (
|
||||
<div className="glass p-5">
|
||||
<h3 className="mb-3 text-xs font-medium uppercase tracking-widest text-gray-500">Sentiment</h3>
|
||||
{latest ? (
|
||||
<div className="space-y-2.5 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Classification</span>
|
||||
<span className={classificationColors[latest.classification] ?? 'text-gray-300'}>
|
||||
{latest.classification}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Confidence</span>
|
||||
<span className="text-gray-200">{formatPercent(latest.confidence)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Dimension Score</span>
|
||||
<span className="text-gray-200">{data.dimension_score !== null ? Math.round(data.dimension_score) : '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">Sources</span>
|
||||
<span className="text-gray-200">{data.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">No sentiment data available</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user