first commit
Deploy / lint (push) Failing after 7s
Deploy / test (push) Has been skipped
Deploy / deploy (push) Has been skipped

This commit is contained in:
Dennis Thiessen
2026-02-20 17:31:01 +01:00
commit 61ab24490d
160 changed files with 17034 additions and 0 deletions
@@ -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>
);
}