186 lines
6.4 KiB
Svelte
186 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
import { screenerStore } from '$lib/stores/screener.store.svelte.js';
|
|
import SignalBadge from '$lib/components/shared/SignalBadge.svelte';
|
|
import Spinner from '$lib/components/shared/Spinner.svelte';
|
|
import VerdictPill from '$lib/components/shared/VerdictPill.svelte';
|
|
import MarketContextStrip from '$lib/components/shared/MarketContextStrip.svelte';
|
|
import AssetTable from '$lib/components/screener/AssetTable.svelte';
|
|
import AnalysisSidebar from '$lib/components/screener/AnalysisSidebar.svelte';
|
|
|
|
const s = screenerStore;
|
|
|
|
let { data: _data } = $props();
|
|
|
|
// Pure UI state — not shared, kept local
|
|
let searchOpen = $state(false);
|
|
|
|
// Boot — fetch catalysts + screen on mount
|
|
let _booted = false;
|
|
$effect(() => {
|
|
if (_booted) return;
|
|
_booted = true;
|
|
s.reloadCatalysts();
|
|
});
|
|
</script>
|
|
|
|
<div class="page">
|
|
|
|
<!-- ── Toolbar ────────────────────────────────────────────────────── -->
|
|
<div class="toolbar">
|
|
<div class="toolbar-top">
|
|
<button onclick={() => s.reloadCatalysts()} disabled={s.loading || s.loadingCats} class="btn-catalyst">
|
|
{#if s.loadingCats}<Spinner size="sm" />{:else}📰 Today Market{/if}
|
|
</button>
|
|
<button
|
|
onclick={() => searchOpen = !searchOpen}
|
|
class="btn-search-toggle"
|
|
title="Screen custom tickers"
|
|
>
|
|
🔍 {searchOpen ? 'Hide search' : 'Search tickers'}
|
|
</button>
|
|
{#if s.screenedAt}
|
|
<span class="screened-at">Last screened {s.screenedAt}</span>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if searchOpen}
|
|
<div class="search-row">
|
|
<input
|
|
bind:value={s.input}
|
|
placeholder="AAPL, MSFT, VOO …"
|
|
onkeydown={e => e.key === 'Enter' && s.screen()}
|
|
/>
|
|
<button onclick={() => s.screen()} disabled={s.loading || s.loadingCats} class="btn-screen">
|
|
{#if s.loading}<Spinner size="sm" />{:else}Screen{/if}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if s.ctx}
|
|
<MarketContextStrip ctx={s.ctx} />
|
|
{/if}
|
|
</div>
|
|
|
|
{#if s.error}
|
|
<div class="error-banner">⚠ {s.error}</div>
|
|
{/if}
|
|
|
|
{#if s.loading || s.loadingCats}
|
|
<div class="loading-area">
|
|
<Spinner size="lg" label={s.loadingCats ? 'Fetching news catalysts…' : 'Screening tickers…'} />
|
|
</div>
|
|
{/if}
|
|
|
|
{#if s.results && !s.loading && !s.loadingCats}
|
|
<!-- ── Signal Summary ───────────────────────────────────────────── -->
|
|
<section class="section">
|
|
<div class="section-header">
|
|
<h2>Signal Summary</h2>
|
|
<span class="count">{s.allAssets.length} assets</span>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th class="col-ticker">Ticker</th>
|
|
<th>Type</th>
|
|
<th>Signal</th>
|
|
<th>Mkt-Adjusted</th>
|
|
<th>Fundamental</th>
|
|
<th title="Market cap tier (stocks only)">Cap</th>
|
|
<th title="Growth / style classification (stocks only)">Style</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each s.allAssets as r}
|
|
{@const dm = r.asset.displayMetrics ?? {}}
|
|
<tr>
|
|
<td class="ticker">{r.asset.ticker}</td>
|
|
<td><span class="tag">{r.asset.type}</span></td>
|
|
<td><SignalBadge signal={r.signal} /></td>
|
|
<td><VerdictPill label={r.inflated.label} /></td>
|
|
<td><VerdictPill label={r.fundamental.label} /></td>
|
|
<td class="dim-cell">{dm['Cap Tier'] ?? '—'}</td>
|
|
<td class="dim-cell">{dm['Style'] ?? '—'}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Per-type detail tables ────────────────────────────────────── -->
|
|
{#each (['STOCK', 'ETF', 'BOND'] as const) as type}
|
|
{#if s.results[type]?.length}
|
|
<AssetTable
|
|
{type}
|
|
rows={s.results[type]}
|
|
analyzeLoading={s.sidebar.loading && s.sidebar.type === type}
|
|
onAnalyze={() => s.runTabAnalysis(type)}
|
|
/>
|
|
{/if}
|
|
{/each}
|
|
|
|
<!-- ── Failed tickers ────────────────────────────────────────────── -->
|
|
{#if s.results.ERROR?.length}
|
|
<section class="section">
|
|
<h2>Failed <span class="count">{s.results.ERROR.length}</span></h2>
|
|
<div class="error-list">
|
|
{#each s.results.ERROR as e}
|
|
<div class="error-item"><span class="ticker">{e.ticker}</span> {e.message}</div>
|
|
{/each}
|
|
</div>
|
|
</section>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<AnalysisSidebar sidebar={s.sidebar} onClose={() => s.closeSidebar()} />
|
|
|
|
<style>
|
|
.page { max-width: 1400px; padding-bottom: 60px; }
|
|
|
|
.toolbar { margin-bottom: 20px; display: flex; flex-direction: column; gap: 10px; }
|
|
.toolbar-top { display: flex; align-items: center; gap: 8px; }
|
|
.search-row { display: flex; gap: 8px; align-items: center; }
|
|
|
|
input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-input);
|
|
border-radius: var(--radius-md);
|
|
color: var(--text-secondary);
|
|
padding: 10px var(--space-lg);
|
|
font-size: var(--fs-md);
|
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
letter-spacing: 0.02em;
|
|
outline: none;
|
|
transition: border-color var(--transition);
|
|
|
|
&:focus { border-color: var(--blue); box-shadow: 0 0 0 2px #3b82f620; }
|
|
}
|
|
|
|
.btn-search-toggle {
|
|
background: var(--bg-card);
|
|
color: var(--text-dim);
|
|
border: 1px solid var(--border-input);
|
|
font-size: 12px;
|
|
padding: 8px var(--space-lg);
|
|
|
|
&:hover { background: #263347; color: var(--text-muted); }
|
|
}
|
|
|
|
.screened-at {
|
|
margin-left: auto;
|
|
font-size: var(--fs-sm);
|
|
color: var(--text-dimmer);
|
|
}
|
|
|
|
.dim-cell { font-size: var(--fs-sm); color: var(--text-dim); white-space: nowrap; }
|
|
|
|
.error-list { padding: 12px var(--space-xl); display: flex; flex-direction: column; gap: 6px; }
|
|
.error-item { color: var(--text-dim); font-size: 12px; }
|
|
.error-item :global(.ticker) { color: var(--red); font-weight: 700; margin-right: 8px; }
|
|
</style>
|