This commit is contained in:
2026-04-20 22:51:41 +02:00
parent 89fc0119f3
commit 81eec53978
27 changed files with 1067 additions and 399 deletions
+2 -2
View File
@@ -43,8 +43,8 @@
:global(*, *::before, *::after) { box-sizing: border-box; }
:global(body) {
margin: 0;
background: #0d1117;
color: #e6edf3;
background: #f6f8fa;
color: #1f2328;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 14px;
}
+6 -6
View File
@@ -37,8 +37,8 @@
nav {
width: 200px;
min-height: 100vh;
background: #0d1117;
border-right: 1px solid #21262d;
background: #24292f;
border-right: 1px solid #32383f;
display: flex;
flex-direction: column;
padding: 1.5rem 0;
@@ -50,7 +50,7 @@
align-items: center;
gap: 0.5rem;
padding: 0 1.25rem 1.5rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #32383f;
margin-bottom: 1rem;
}
@@ -76,14 +76,14 @@
align-items: center;
gap: 0.75rem;
padding: 0.6rem 1.25rem;
color: #8b949e;
color: #adbac7;
text-decoration: none;
font-size: 0.9rem;
transition: background 0.15s, color 0.15s;
}
li a:hover { background: #161b22; color: #e6edf3; }
li.active a { background: #161b22; color: #58a6ff; }
li a:hover { background: #2d333b; color: #e6edf3; }
li.active a { background: #2d333b; color: #58a6ff; }
.icon { font-size: 1rem; width: 1.2rem; text-align: center; }
</style>
+6 -6
View File
@@ -23,15 +23,15 @@
padding: 0.75rem 1.25rem;
border-radius: 6px;
font-size: 0.875rem;
color: #e6edf3;
background: #21262d;
border-left: 3px solid #58a6ff;
color: #1f2328;
background: #d0d7de;
border-left: 3px solid #0969da;
animation: slide-in 0.2s ease;
}
.notif.success { border-color: #3fb950; }
.notif.error { border-color: #f85149; }
.notif.warning { border-color: #d29922; }
.notif.success { border-color: #1a7f37; }
.notif.error { border-color: #cf222e; }
.notif.warning { border-color: #9a6700; }
@keyframes slide-in {
from { opacity: 0; transform: translateX(1rem); }
+2
View File
@@ -30,6 +30,8 @@ export const api = {
getDiscovery: (minScore) => request('GET', `/discover?min_score=${minScore ?? 30}`),
runDiscovery: () => request('POST', '/discover/run'),
discoveryStatus: () => request('GET', '/discover/status'),
analyzeDeep: (tickers) => request('POST', '/discover/analyze', { tickers }),
analyzeStatus: () => request('GET', '/discover/analyze/status'),
getInsiderTrades: (ticker) => request('GET', `/insider-trades${ticker ? `?ticker=${ticker}` : ''}`),
syncInsider: () => request('POST', '/insider-trades/sync'),
}
+28 -28
View File
@@ -143,7 +143,7 @@
</div>
<style>
.page h1 { color: #e6edf3; font-size: 1.4rem; }
.page h1 { color: #1f2328; font-size: 1.4rem; }
.cards {
display: grid;
@@ -153,23 +153,23 @@
}
.card {
background: #161b22;
border: 1px solid #21262d;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 1.25rem;
}
.card.highlight { border-color: #d29922; }
.card.highlight { border-color: #9a6700; }
.card-label { font-size: 0.72rem; color: #8b949e; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
.card-value { font-size: 1.25rem; font-weight: 600; color: #e6edf3; }
.card-value.green { color: #3fb950; }
.card-value.red { color: #f85149; }
.card-value.orange { color: #d29922; }
.card-label { font-size: 0.72rem; color: #57606a; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }
.card-value { font-size: 1.25rem; font-weight: 600; color: #1f2328; }
.card-value.green { color: #1a7f37; }
.card-value.red { color: #cf222e; }
.card-value.orange { color: #9a6700; }
.sep { color: #484f58; }
.sep { color: #8c959f; }
.section { margin-bottom: 2rem; }
.section h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.06em; color: #8b949e; border-bottom: 1px solid #21262d; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.section h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.06em; color: #57606a; border-bottom: 1px solid #d0d7de; padding-bottom: 0.5rem; margin-bottom: 1rem; }
.alert-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
.alert-row {
@@ -177,14 +177,14 @@
align-items: center;
gap: 1rem;
padding: 0.6rem 1rem;
background: #161b22;
border: 1px solid #21262d;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 6px;
}
.ticker { font-weight: 600; color: #58a6ff; min-width: 60px; }
.price { color: #e6edf3; font-variant-numeric: tabular-nums; }
.rsi { font-size: 0.8rem; color: #8b949e; }
.ticker { font-weight: 600; color: #0969da; min-width: 60px; }
.price { color: #1f2328; font-variant-numeric: tabular-nums; }
.rsi { font-size: 0.8rem; color: #57606a; }
.chg { font-size: 0.8rem; margin-left: auto; font-variant-numeric: tabular-nums; }
.badge {
@@ -193,31 +193,31 @@
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: #21262d;
color: #8b949e;
background: #d0d7de;
color: #57606a;
}
.badge.green { background: #0d2c1a; color: #3fb950; }
.badge.red { background: #2c0d0d; color: #f85149; }
.badge.green { background: #dafbe1; color: #1a7f37; }
.badge.red { background: #ffebe9; color: #cf222e; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th {
text-align: left;
color: #8b949e;
color: #57606a;
font-weight: 500;
padding: 0.45rem 0.75rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #d0d7de;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
th.num { text-align: right; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #161b22; color: #c9d1d9; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #ffffff; color: #2c3138; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
tr:hover td { background: #161b22; }
tr:hover td { background: #ffffff; }
.green { color: #3fb950; }
.red { color: #f85149; }
.green { color: #1a7f37; }
.red { color: #cf222e; }
.empty { color: #484f58; font-size: 0.875rem; }
.empty a { color: #58a6ff; }
.empty { color: #8c959f; font-size: 0.875rem; }
.empty a { color: #0969da; }
</style>
+165 -62
View File
@@ -7,21 +7,41 @@
let etoroStatus = { syncing: false, count: 0, progress: 0, total: 0, last_error: '' }
// Statut discovery
let discStatus = { running: false, progress: 0, total: 0, found: 0 }
// Statut analyse en profondeur
let anaStatus = { running: false, progress: 0, total: 0 }
// Résultats
let results = []
let loading = false
let minScore = 30
let filterAlert = ''
// Sélection pour analyse en profondeur
let selected = new Set()
$: allSelected = filtered.length > 0 && filtered.every(r => selected.has(r.ticker))
function toggleAll() {
if (allSelected) {
filtered.forEach(r => selected.delete(r.ticker))
} else {
filtered.forEach(r => selected.add(r.ticker))
}
selected = new Set(selected)
}
function toggleOne(ticker) {
if (selected.has(ticker)) selected.delete(ticker)
else selected.add(ticker)
selected = new Set(selected)
}
let pollInterval = null
onMount(async () => {
await refreshStatus()
await loadResults()
// Poll toutes les 3s si un process tourne
pollInterval = setInterval(async () => {
await refreshStatus()
if (discStatus.running) await loadResults()
if (discStatus.running || anaStatus.running) await loadResults()
}, 3000)
})
@@ -29,13 +49,31 @@
async function refreshStatus() {
try {
[etoroStatus, discStatus] = await Promise.all([
[etoroStatus, discStatus, anaStatus] = await Promise.all([
api.etoroStatus(),
api.discoveryStatus(),
api.analyzeStatus(),
])
} catch {}
}
async function analyzeDeep() {
const tickers = [...selected]
if (tickers.length === 0) return
try {
const r = await api.analyzeDeep(tickers)
if (r.status === 'already_running') {
notify('Une analyse est déjà en cours…', 'info')
} else {
notify(`Analyse EDGAR + market cap lancée sur ${tickers.length} ticker(s)…`, 'info')
selected = new Set()
}
await refreshStatus()
} catch(e) {
notify('Erreur : ' + e.message, 'error')
}
}
async function loadResults() {
loading = true
try {
@@ -66,7 +104,7 @@
if (r.status === 'already_running') {
notify('Scan déjà en cours…', 'info')
} else {
notify(`Scan lancé sur ${etoroStatus.count.toLocaleString()} tickers eToro — ~${Math.ceil(etoroStatus.count * 0.12 / 60)} min`, 'info')
notify(`Scan lancé sur ${etoroStatus.count.toLocaleString()} tickers eToro — ~${estMinutes} min`, 'info')
}
await refreshStatus()
} catch(e) {
@@ -127,6 +165,10 @@
$: etoroPct = etoroStatus.total > 0
? Math.round(etoroStatus.progress / etoroStatus.total * 100)
: 0
// Estimation stable : total API (fixe dès la 1ère page), pas le count DB qui monte
$: scanBase = etoroStatus.total > 0 ? etoroStatus.total : etoroStatus.count
$: estMinutes = Math.ceil(scanBase * 0.12 / 60)
</script>
<div class="page">
@@ -169,6 +211,22 @@
</div>
</div>
<div class="status-block" class:active={!anaStatus.running && anaStatus.total > 0} class:syncing={anaStatus.running}>
<div class="status-label">Analyse en profondeur</div>
<div class="status-value">
{#if anaStatus.running}
{@const anaPct = Math.round(anaStatus.progress / anaStatus.total * 100)}
<span class="spinner"></span>
{anaStatus.progress} / {anaStatus.total}
<div class="progress-bar"><div class="progress-fill orange" style="width:{anaPct}%"></div></div>
{:else if anaStatus.total > 0}
<span class="ok">{anaStatus.total} analysés</span>
{:else}
<span class="empty-val"></span>
{/if}
</div>
</div>
<div class="status-actions">
<button class="btn-etoro" on:click={syncEtoro} disabled={etoroStatus.syncing}>
{etoroStatus.syncing ? '↻ Chargement…' : '↻ Sync eToro'}
@@ -210,17 +268,26 @@
<p>Commence par <strong>Sync eToro</strong> pour charger la liste des instruments disponibles (~3 000 stocks).</p>
{:else}
<p>Clique <strong>Lancer le scan</strong> pour analyser les {etoroStatus.count.toLocaleString()} stocks eToro et trouver les meilleures opportunités.</p>
<p class="hint">Durée estimée : ~{Math.ceil(etoroStatus.count * 0.12 / 60)} minutes</p>
<p class="hint">Durée estimée : ~{estMinutes} minutes</p>
{/if}
</div>
{:else}
<div class="results-header">
<span class="count">{filtered.length} opportunités (score ≥ {minScore})</span>
{#if selected.size > 0}
<button class="btn-analyze" on:click={analyzeDeep} disabled={anaStatus.running}>
{anaStatus.running ? '↻ Analyse en cours…' : `⚡ Analyser en profondeur (${selected.size})`}
</button>
<button class="btn-clear-sel" on:click={() => selected = new Set()}> Désélectionner</button>
{/if}
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="col-check">
<input type="checkbox" checked={allSelected} on:change={toggleAll} />
</th>
<th>Score</th>
<th>Ticker</th>
<th class="num">Prix</th>
@@ -236,7 +303,11 @@
<tbody>
{#each filtered as r}
{@const ratio = volRatio(r.volume, r.avg_volume20)}
<tr class:has-alert={r.alert}>
<tr class:has-alert={r.alert} class:selected={selected.has(r.ticker)}>
<td class="col-check">
<input type="checkbox" checked={selected.has(r.ticker)}
on:change={() => toggleOne(r.ticker)} />
</td>
<td><span class="score-pill {scoreClass(r.score)}">{r.score}</span></td>
<td>
<div class="ticker">{r.ticker}</div>
@@ -275,8 +346,8 @@
</div>
<style>
.page h1 { color: #e6edf3; font-size: 1.4rem; margin: 0 0 0.25rem; }
.subtitle { color: #484f58; font-size: 0.85rem; margin: 0 0 1.5rem; }
.page h1 { color: #1f2328; font-size: 1.4rem; margin: 0 0 0.25rem; }
.subtitle { color: #8c959f; font-size: 0.85rem; margin: 0 0 1.5rem; }
/* Status bar */
.status-bar {
@@ -285,8 +356,8 @@
align-items: flex-start;
margin-bottom: 1.25rem;
padding: 1.1rem 1.25rem;
background: #161b22;
border: 1px solid #21262d;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
flex-wrap: wrap;
}
@@ -298,7 +369,7 @@
.status-label {
font-size: 0.72rem;
color: #8b949e;
color: #57606a;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.4rem;
@@ -306,46 +377,47 @@
.status-value {
font-size: 0.9rem;
color: #484f58;
color: #8c959f;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.status-block.active .status-value { color: #c9d1d9; }
.status-block.active .status-value { color: #2c3138; }
.ok { color: #3fb950; }
.empty-val { color: #30363d; }
.ok { color: #1a7f37; }
.empty-val { color: #c6cdd5; }
.spinner {
display: inline-block;
animation: spin 1s linear infinite;
color: #58a6ff;
color: #0969da;
}
@keyframes spin { to { transform: rotate(360deg); } }
.found-count { color: #3fb950; font-size: 0.8rem; }
.found-count { color: #1a7f37; font-size: 0.8rem; }
.progress-bar {
width: 100%;
height: 3px;
background: #21262d;
background: #d0d7de;
border-radius: 2px;
margin-top: 0.4rem;
flex-basis: 100%;
}
.progress-fill {
height: 100%;
background: #58a6ff;
background: #0969da;
border-radius: 2px;
transition: width 0.5s ease;
}
.progress-fill.green { background: #3fb950; }
.progress-fill.green { background: #1a7f37; }
.progress-fill.orange { background: #9a6700; }
.error-hint {
font-size: 0.72rem;
color: #f85149;
color: #cf222e;
margin-top: 0.3rem;
}
@@ -366,11 +438,11 @@
cursor: pointer;
}
.btn-etoro {
background: #21262d;
color: #8b949e;
border: 1px solid #30363d;
background: #d0d7de;
color: #57606a;
border: 1px solid #c6cdd5;
}
.btn-etoro:hover:not(:disabled) { color: #e6edf3; }
.btn-etoro:hover:not(:disabled) { color: #1f2328; }
.btn-discover {
background: #1f6feb;
color: #fff;
@@ -386,50 +458,81 @@
margin-bottom: 1rem;
}
.filter-group { display: flex; flex-direction: column; gap: 0.3rem; }
label { font-size: 0.72rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.04em; }
label { font-size: 0.72rem; color: #57606a; text-transform: uppercase; letter-spacing: 0.04em; }
input, select {
background: #161b22;
border: 1px solid #30363d;
background: #ffffff;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #e6edf3;
color: #1f2328;
padding: 0.4rem 0.6rem;
font-size: 0.875rem;
outline: none;
width: 100px;
}
input:focus, select:focus { border-color: #58a6ff; }
select option { background: #161b22; }
input:focus, select:focus { border-color: #0969da; }
select option { background: #ffffff; }
.btn-refresh {
background: #21262d;
border: 1px solid #30363d;
background: #d0d7de;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #8b949e;
color: #57606a;
padding: 0.4rem 0.65rem;
align-self: flex-end;
}
.btn-refresh:hover { color: #e6edf3; }
.btn-refresh:hover { color: #1f2328; }
.results-header { margin-bottom: 0.75rem; }
.count { font-size: 0.78rem; color: #484f58; }
.results-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-wrap: wrap;
}
.count { font-size: 0.78rem; color: #8c959f; }
.btn-analyze {
background: #9a6700;
color: #fff;
border: none;
border-radius: 6px;
padding: 0.35rem 0.85rem;
font-size: 0.8rem;
font-weight: 500;
}
.btn-analyze:hover:not(:disabled) { background: #7d5500; }
.btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-clear-sel {
background: none;
border: 1px solid #d0d7de;
border-radius: 6px;
color: #57606a;
padding: 0.35rem 0.65rem;
font-size: 0.78rem;
}
.btn-clear-sel:hover { color: #1f2328; }
.col-check { width: 32px; padding: 0.45rem 0.5rem; }
tr.selected td { background: #fff8e6; }
/* Table */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; white-space: nowrap; }
th {
text-align: left;
color: #8b949e;
color: #57606a;
font-weight: 500;
padding: 0.45rem 0.75rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #d0d7de;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
th.num { text-align: right; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #161b22; color: #c9d1d9; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #ffffff; color: #2c3138; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
tr:hover td { background: #161b22; }
tr.has-alert td { background: #0d180d; }
tr:hover td { background: #ffffff; }
tr.has-alert td { background: #eafbec; }
.score-pill {
display: inline-block;
@@ -440,19 +543,19 @@
min-width: 30px;
text-align: center;
}
.score-pill.high { background: #0d3320; color: #3fb950; }
.score-pill.mid { background: #2d2200; color: #d29922; }
.score-pill.low { background: #1c1c1c; color: #484f58; }
.score-pill.high { background: #dafbe1; color: #1a7f37; }
.score-pill.mid { background: #fff8c5; color: #9a6700; }
.score-pill.low { background: #eaeef2; color: #8c959f; }
.ticker { font-weight: 600; color: #58a6ff; }
.ticker-name { font-size: 0.72rem; color: #484f58; }
.ticker { font-weight: 600; color: #0969da; }
.ticker-name { font-size: 0.72rem; color: #8c959f; }
.green { color: #3fb950; }
.red { color: #f85149; }
.rsi.oversold { color: #3fb950; font-weight: 600; }
.rsi.overbought { color: #f85149; font-weight: 600; }
.vol-spike { color: #d29922; font-weight: 600; }
.deep-value { color: #d29922; font-weight: 600; }
.green { color: #1a7f37; }
.red { color: #cf222e; }
.rsi.oversold { color: #1a7f37; font-weight: 600; }
.rsi.overbought { color: #cf222e; font-weight: 600; }
.vol-spike { color: #9a6700; font-weight: 600; }
.deep-value { color: #9a6700; font-weight: 600; }
.badge {
display: inline-block;
@@ -460,18 +563,18 @@
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: #21262d;
color: #8b949e;
background: #d0d7de;
color: #57606a;
}
.badge.green { background: #0d2c1a; color: #3fb950; }
.badge.red { background: #2c0d0d; color: #f85149; }
.badge.orange { background: #2d1f00; color: #d29922; }
.badge.green { background: #dafbe1; color: #1a7f37; }
.badge.red { background: #ffebe9; color: #cf222e; }
.badge.orange { background: #fff3c4; color: #9a6700; }
.empty-state { text-align: center; padding: 3rem; color: #484f58; }
.empty-state { text-align: center; padding: 3rem; color: #8c959f; }
.empty-state .icon { font-size: 2.5rem; margin-bottom: 1rem; }
.empty-state p { font-size: 0.9rem; margin: 0.4rem 0; }
.empty-state strong { color: #8b949e; }
.empty-state .hint { font-size: 0.8rem; color: #30363d; }
.empty-state strong { color: #57606a; }
.empty-state .hint { font-size: 0.8rem; color: #c6cdd5; }
.muted { color: #484f58; font-size: 0.875rem; }
.muted { color: #8c959f; font-size: 0.875rem; }
</style>
+38 -38
View File
@@ -209,7 +209,7 @@
.tab {
background: none;
border: none;
color: #484f58;
color: #8c959f;
font-size: 1rem;
font-weight: 600;
padding: 0.4rem 0.75rem;
@@ -217,13 +217,13 @@
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.tab:hover { color: #8b949e; background: #161b22; }
.tab.active { color: #e6edf3; background: #161b22; }
.tab:hover { color: #57606a; background: #ffffff; }
.tab.active { color: #1f2328; background: #ffffff; }
.badge-count {
display: inline-block;
background: #21262d;
color: #8b949e;
background: #d0d7de;
color: #57606a;
font-size: 0.7rem;
font-weight: 600;
padding: 0.1rem 0.4rem;
@@ -231,7 +231,7 @@
margin-left: 0.3rem;
vertical-align: middle;
}
.badge-count.badge-orange { background: #2d1f00; color: #d29922; }
.badge-count.badge-orange { background: #2d1f00; color: #9a6700; }
.toolbar {
display: flex;
@@ -240,23 +240,23 @@
}
input {
background: #161b22;
border: 1px solid #30363d;
background: #ffffff;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #e6edf3;
color: #1f2328;
padding: 0.45rem 0.75rem;
font-size: 0.875rem;
outline: none;
width: 200px;
}
input:focus { border-color: #58a6ff; }
input::placeholder { color: #484f58; }
input:focus { border-color: #0969da; }
input::placeholder { color: #8c959f; }
.btn-sync {
background: #21262d;
border: 1px solid #30363d;
background: #d0d7de;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #58a6ff;
color: #0969da;
border-color: #1f6feb;
padding: 0.45rem 0.85rem;
font-size: 0.8rem;
@@ -269,12 +269,12 @@
.news-list { display: flex; flex-direction: column; gap: 0.6rem; }
.news-card {
background: #161b22;
border: 1px solid #21262d;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
padding: 0.85rem 1.1rem;
}
.news-card:hover { border-color: #30363d; }
.news-card:hover { border-color: #c6cdd5; }
.news-meta {
display: flex;
@@ -283,17 +283,17 @@
margin-bottom: 0.35rem;
font-size: 0.75rem;
}
.ticker { color: #58a6ff; font-weight: 600; }
.source { color: #8b949e; }
.time { color: #484f58; margin-left: auto; }
.ticker { color: #0969da; font-weight: 600; }
.source { color: #57606a; }
.time { color: #8c959f; margin-left: auto; }
.sentiment { padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.7rem; font-weight: 500; }
.sentiment.green { background: #0d2c1a; color: #3fb950; }
.sentiment.red { background: #2c0d0d; color: #f85149; }
.sentiment.green { background: #0d2c1a; color: #1a7f37; }
.sentiment.red { background: #2c0d0d; color: #cf222e; }
.news-headline { font-size: 0.875rem; color: #c9d1d9; line-height: 1.4; }
.news-headline a { color: #c9d1d9; text-decoration: none; }
.news-headline a:hover { color: #58a6ff; }
.news-headline { font-size: 0.875rem; color: #2c3138; line-height: 1.4; }
.news-headline a { color: #2c3138; text-decoration: none; }
.news-headline a:hover { color: #0969da; }
/* Insider trades */
.table-wrap { overflow-x: auto; }
@@ -302,26 +302,26 @@
th {
text-align: left;
color: #8b949e;
color: #57606a;
font-weight: 500;
padding: 0.45rem 0.75rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #d0d7de;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
th.num { text-align: right; }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #161b22; color: #c9d1d9; }
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid #ffffff; color: #2c3138; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
tr:hover td { background: #161b22; }
tr:hover td { background: #ffffff; }
tr.purchase td { background: #0a1a0a; }
tr.purchase:hover td { background: #0d2210; }
td.ticker { font-weight: 600; color: #58a6ff; }
td.title-col { color: #8b949e; font-size: 0.78rem; }
td.date { color: #8b949e; }
td.ticker { font-weight: 600; color: #0969da; }
td.title-col { color: #57606a; font-size: 0.78rem; }
td.date { color: #57606a; }
.tx-badge {
display: inline-block;
@@ -329,19 +329,19 @@
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: #21262d;
color: #8b949e;
background: #d0d7de;
color: #57606a;
}
.tx-badge.green { background: #0d2c1a; color: #3fb950; }
.tx-badge.green { background: #0d2c1a; color: #1a7f37; }
.value.big { color: #d29922; font-weight: 600; }
.value.big { color: #9a6700; font-weight: 600; }
.link-sec {
color: #58a6ff;
color: #0969da;
text-decoration: none;
font-size: 0.78rem;
}
.link-sec:hover { text-decoration: underline; }
.empty, .muted { color: #484f58; font-size: 0.875rem; }
.empty, .muted { color: #8c959f; font-size: 0.875rem; }
</style>
+92 -57
View File
@@ -19,7 +19,7 @@
onMount(async () => {
load()
try {
const stats = await api.etoroStats()
const stats = await api.etoroStatus()
etoroCount = stats.instruments ?? 0
} catch {}
})
@@ -54,7 +54,7 @@
await api.syncEtoro()
notify('Sync eToro lancé — ~5000 instruments à charger', 'info')
setTimeout(async () => {
const stats = await api.etoroStats()
const stats = await api.etoroStatus()
etoroCount = stats.instruments ?? 0
}, 8000)
} catch(e) {
@@ -87,23 +87,35 @@
function alertLabel(alert) {
const map = {
mega_insider_buy: '🐋 Mega Insider',
ceo_change: '👔 New CEO',
deep_value_reversal: '📉→↑ Deep Value',
oversold: '▼ Oversold',
overbought: '▲ Overbought',
macd_cross_up: '↑ MACD bull',
macd_cross_down: '↓ MACD bear',
volume_spike: '⚡ Vol spike',
insider_sell: '⚠ Insider sell',
}
return map[alert] || alert
}
function alertClass(alert) {
if (['mega_insider_buy','oversold','macd_cross_up','deep_value_reversal'].includes(alert)) return 'green'
if (['overbought','macd_cross_down'].includes(alert)) return 'red'
if (['mega_insider_buy','oversold','macd_cross_up','deep_value_reversal','ceo_change'].includes(alert)) return 'green'
if (['overbought','macd_cross_down','insider_sell'].includes(alert)) return 'red'
if (alert === 'volume_spike') return 'orange'
return ''
}
function earningsDaysLabel(dateStr) {
if (!dateStr) return ''
const d = new Date(dateStr)
const now = new Date()
const diff = Math.round((d - now) / 86400000)
if (diff < 0 || diff > 90) return ''
if (diff === 0) return 'Auj.'
return diff + 'j'
}
function fmtInsider(v) {
if (!v) return ''
if (v >= 1e6) return '$' + (v/1e6).toFixed(1) + 'M'
@@ -199,6 +211,8 @@
<option value="macd_cross_up">↑ MACD bull</option>
<option value="macd_cross_down">↓ MACD bear</option>
<option value="volume_spike">⚡ Vol spike</option>
<option value="ceo_change">👔 New CEO</option>
<option value="insider_sell">⚠ Insider sell</option>
</select>
</div>
@@ -229,7 +243,9 @@
<th class="num">MACD histo</th>
<th class="num">Vol/Avg</th>
<th class="num">52w%</th>
<th class="num">Insider 30j</th>
<th class="num">Insider Buy</th>
<th class="num">Insider Sell</th>
<th class="num">Earnings</th>
<th class="num">Mkt Cap</th>
<th class="num">Short</th>
<th>Alerte</th>
@@ -249,7 +265,7 @@
<div class="ticker-name">{s.name}</div>
{/if}
</td>
<td><span class="cap-label">{capLabel(s.market_cap)}</span></td>
<td>{#if capLabel(s.market_cap)}<span class="cap-label">{capLabel(s.market_cap)}</span>{:else}{/if}</td>
<td class="num">${fmt(s.price)}</td>
<td class="num" class:green={s.change_pct > 0} class:red={s.change_pct < 0}>
{s.change_pct > 0 ? '+' : ''}{fmt(s.change_pct)}%
@@ -267,6 +283,14 @@
<td class="num" class:insider-big={s.insider_value_30d >= 1_000_000}>
{fmtInsider(s.insider_value_30d)}
</td>
<td class="num" class:insider-sell-big={s.insider_sell_value_30d >= 1_000_000}>
{fmtInsider(s.insider_sell_value_30d)}
</td>
<td class="num">
{#if earningsDaysLabel(s.earnings_date)}
<span class="earnings-badge">{earningsDaysLabel(s.earnings_date)}</span>
{:else}{/if}
</td>
<td class="num">{fmtCap(s.market_cap)}</td>
<td class="num">{s.short_ratio > 0 ? fmt(s.short_ratio, 1) + 'd' : '—'}</td>
<td>
@@ -286,7 +310,7 @@
</div>
<style>
.page h1 { color: #e6edf3; font-size: 1.4rem; margin: 0; }
.page h1 { color: #1f2328; font-size: 1.4rem; margin: 0; }
.header {
display: flex;
@@ -299,20 +323,20 @@
.etoro-badge {
font-size: 0.78rem;
color: #484f58;
border: 1px solid #21262d;
color: #8c959f;
border: 1px solid #d0d7de;
border-radius: 6px;
padding: 0.35rem 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.etoro-badge.active { color: #3fb950; border-color: #3fb950; }
.etoro-badge.active { color: #1a7f37; border-color: #1a7f37; }
.btn-load-etoro {
background: none;
border: none;
color: #58a6ff;
color: #0969da;
font-size: 0.78rem;
padding: 0;
cursor: pointer;
@@ -337,8 +361,8 @@
align-items: flex-end;
margin-bottom: 1.25rem;
padding: 1rem 1.25rem;
background: #161b22;
border: 1px solid #21262d;
background: #ffffff;
border: 1px solid #d0d7de;
border-radius: 8px;
}
@@ -348,40 +372,40 @@
gap: 0.4rem;
cursor: pointer;
font-size: 0.875rem;
color: #8b949e;
color: #57606a;
align-self: flex-end;
padding-bottom: 0.1rem;
}
.toggle input { accent-color: #3fb950; }
.toggle input { accent-color: #1a7f37; }
.filter-group { display: flex; flex-direction: column; gap: 0.3rem; }
label { font-size: 0.72rem; color: #8b949e; text-transform: uppercase; letter-spacing: 0.05em; }
label { font-size: 0.72rem; color: #57606a; text-transform: uppercase; letter-spacing: 0.05em; }
input[type="number"], select {
background: #0d1117;
border: 1px solid #30363d;
background: #f6f8fa;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #e6edf3;
color: #1f2328;
padding: 0.4rem 0.6rem;
font-size: 0.875rem;
outline: none;
width: 110px;
}
input:focus, select:focus { border-color: #58a6ff; }
select option { background: #161b22; }
input:focus, select:focus { border-color: #0969da; }
select option { background: #ffffff; }
.btn-reset {
background: none;
border: 1px solid #30363d;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #8b949e;
color: #57606a;
padding: 0.4rem 0.75rem;
font-size: 0.8rem;
align-self: flex-end;
}
.btn-reset:hover { color: #e6edf3; }
.btn-reset:hover { color: #1f2328; }
.count { font-size: 0.78rem; color: #484f58; margin-bottom: 0.75rem; }
.count { font-size: 0.78rem; color: #8c959f; margin-bottom: 0.75rem; }
.table-wrap { overflow-x: auto; }
@@ -389,24 +413,24 @@
th {
text-align: left;
color: #8b949e;
color: #57606a;
font-weight: 500;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #d0d7de;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
th.num { text-align: right; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #161b22; color: #c9d1d9; }
td { padding: 0.55rem 0.75rem; border-bottom: 1px solid #ffffff; color: #2c3138; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
td.center { text-align: center; }
tr:hover td { background: #161b22; }
tr.has-alert td { background: #0d180d; }
tr.high-score td { background: #0d1a10; }
tr.high-score:hover td { background: #112214; }
tr:hover td { background: #ffffff; }
tr.has-alert td { background: #eafbec; }
tr.high-score td { background: #f0fdf4; }
tr.high-score:hover td { background: #dcfce7; }
.score-pill {
display: inline-block;
@@ -417,28 +441,28 @@
min-width: 32px;
text-align: center;
}
.score-high { background: #0d3320; color: #3fb950; }
.score-mid { background: #2d2200; color: #d29922; }
.score-low { background: #1c1c1c; color: #484f58; }
.score-high { background: #dafbe1; color: #1a7f37; }
.score-mid { background: #fff8c5; color: #9a6700; }
.score-low { background: #eaeef2; color: #8c959f; }
.ticker { font-weight: 600; color: #58a6ff; }
.ticker-name { font-size: 0.72rem; color: #484f58; margin-top: 1px; }
.ticker { font-weight: 600; color: #0969da; }
.ticker-name { font-size: 0.72rem; color: #8c959f; margin-top: 1px; }
.cap-label {
font-size: 0.7rem;
color: #8b949e;
border: 1px solid #30363d;
color: #57606a;
border: 1px solid #c6cdd5;
border-radius: 3px;
padding: 0.1rem 0.35rem;
}
.green { color: #3fb950; }
.red { color: #f85149; }
.green { color: #1a7f37; }
.red { color: #cf222e; }
.rsi.oversold { color: #3fb950; font-weight: 600; }
.rsi.overbought { color: #f85149; font-weight: 600; }
.rsi.oversold { color: #1a7f37; font-weight: 600; }
.rsi.overbought { color: #cf222e; font-weight: 600; }
.vol-spike { color: #d29922; font-weight: 600; }
.vol-spike { color: #9a6700; font-weight: 600; }
.badge {
display: inline-block;
@@ -446,25 +470,36 @@
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: #21262d;
color: #8b949e;
background: #d0d7de;
color: #57606a;
}
.badge.green { background: #0d2c1a; color: #3fb950; }
.badge.red { background: #2c0d0d; color: #f85149; }
.badge.orange { background: #2d1f00; color: #d29922; }
.badge.green { background: #dafbe1; color: #1a7f37; }
.badge.red { background: #ffebe9; color: #cf222e; }
.badge.orange { background: #fff3c4; color: #9a6700; }
.etoro-dot { color: #3fb950; font-size: 0.9rem; }
.etoro-dot { color: #1a7f37; font-size: 0.9rem; }
.pct-from-high { color: #8b949e; }
.pct-from-high.deep-value { color: #d29922; font-weight: 600; }
.pct-from-high { color: #57606a; }
.pct-from-high.deep-value { color: #9a6700; font-weight: 600; }
.insider-big { color: #3fb950; font-weight: 700; }
.insider-big { color: #1a7f37; font-weight: 700; }
.insider-sell-big { color: #cf222e; font-weight: 700; }
.empty-state { text-align: center; padding: 3rem; color: #484f58; }
.earnings-badge {
display: inline-block;
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 600;
background: #fff3c4;
color: #9a6700;
}
.empty-state { text-align: center; padding: 3rem; color: #8c959f; }
.empty-state .icon { font-size: 2.5rem; margin-bottom: 1rem; }
.empty-state p { font-size: 0.9rem; }
.empty-state a { color: #58a6ff; }
.empty-state strong { color: #8b949e; }
.empty-state a { color: #0969da; }
.empty-state strong { color: #57606a; }
.muted { color: #484f58; font-size: 0.875rem; }
.muted { color: #8c959f; font-size: 0.875rem; }
</style>
+21 -18
View File
@@ -4,9 +4,10 @@
import { notify } from '../lib/store.js'
const providers = [
{ key: 'finnhub_api_key', label: 'Finnhub', provider: 'finnhub', placeholder: 'c_xxxxxxxxxxxxxxxx' },
{ key: 'alphavantage_key', label: 'Alpha Vantage', provider: 'alphavantage', placeholder: 'XXXXXXXXXXXX' },
{ key: 'etoro_api_key', label: 'eToro', provider: 'etoro', placeholder: 'eToro API key' },
{ key: 'finnhub_api_key', label: 'Finnhub', provider: 'finnhub', placeholder: 'c_xxxxxxxxxxxxxxxx' },
{ key: 'alphavantage_key', label: 'Alpha Vantage', provider: 'alphavantage', placeholder: 'XXXXXXXXXXXX' },
{ key: 'etoro_api_key', label: 'eToro — Public Key', provider: 'etoro', placeholder: 'x-api-key (clé publique)' },
{ key: 'etoro_user_key', label: 'eToro — User Key', provider: null, placeholder: 'eyJjaSI6… (clé utilisateur)' },
]
let values = {}
@@ -76,6 +77,7 @@
placeholder={p.placeholder}
autocomplete="off"
/>
{#if p.provider}
<button
type="button"
class="btn-test"
@@ -86,6 +88,7 @@
>
{testing[p.provider] ? '…' : testResults[p.provider] === 'ok' ? '✓' : testResults[p.provider] === 'error' ? '✗' : 'Tester'}
</button>
{/if}
</div>
</div>
{/each}
@@ -100,47 +103,47 @@
</div>
<style>
.page h1 { color: #e6edf3; font-size: 1.4rem; }
.page h1 { color: #1f2328; font-size: 1.4rem; }
form { max-width: 560px; }
section { margin-bottom: 2rem; }
h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.08em; color: #8b949e; border-bottom: 1px solid #21262d; padding-bottom: 0.5rem; margin-bottom: 1.25rem; }
h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.08em; color: #57606a; border-bottom: 1px solid #d0d7de; padding-bottom: 0.5rem; margin-bottom: 1.25rem; }
.hint { font-size: 0.8rem; color: #484f58; margin: -0.75rem 0 1.25rem; }
.hint { font-size: 0.8rem; color: #8c959f; margin: -0.75rem 0 1.25rem; }
.field { margin-bottom: 1.25rem; }
label { display: block; font-size: 0.875rem; color: #8b949e; margin-bottom: 0.4rem; }
label { display: block; font-size: 0.875rem; color: #57606a; margin-bottom: 0.4rem; }
.input-row { display: flex; gap: 0.5rem; }
input {
flex: 1;
background: #161b22;
border: 1px solid #30363d;
background: #ffffff;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #e6edf3;
color: #1f2328;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
outline: none;
transition: border-color 0.15s;
}
input:focus { border-color: #58a6ff; }
input::placeholder { color: #484f58; }
input:focus { border-color: #0969da; }
input::placeholder { color: #8c959f; }
.btn-test {
background: #21262d;
border: 1px solid #30363d;
background: #d0d7de;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #8b949e;
color: #57606a;
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
min-width: 64px;
transition: background 0.15s, color 0.15s;
}
.btn-test:hover:not(:disabled) { background: #30363d; color: #e6edf3; }
.btn-test.ok { color: #3fb950; border-color: #3fb950; }
.btn-test.error { color: #f85149; border-color: #f85149; }
.btn-test:hover:not(:disabled) { background: #c6cdd5; color: #1f2328; }
.btn-test.ok { color: #1a7f37; border-color: #1a7f37; }
.btn-test.error { color: #cf222e; border-color: #cf222e; }
.btn-test:disabled { opacity: 0.5; }
.actions { padding-top: 1rem; }
+15 -15
View File
@@ -94,7 +94,7 @@
</div>
<style>
.page h1 { color: #e6edf3; font-size: 1.4rem; }
.page h1 { color: #1f2328; font-size: 1.4rem; }
.add-form {
display: flex;
@@ -105,17 +105,17 @@
input {
flex: 1;
background: #161b22;
border: 1px solid #30363d;
background: #ffffff;
border: 1px solid #c6cdd5;
border-radius: 6px;
color: #e6edf3;
color: #1f2328;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
outline: none;
text-transform: uppercase;
}
input:focus { border-color: #58a6ff; }
input::placeholder { color: #484f58; text-transform: none; }
input:focus { border-color: #0969da; }
input::placeholder { color: #8c959f; text-transform: none; }
.btn-primary {
background: #1f6feb;
@@ -137,10 +137,10 @@
th {
text-align: left;
color: #8b949e;
color: #57606a;
font-weight: 500;
padding: 0.5rem 1rem;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid #d0d7de;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
@@ -148,24 +148,24 @@
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #161b22;
color: #e6edf3;
border-bottom: 1px solid #ffffff;
color: #1f2328;
}
tr:hover td { background: #161b22; }
tr:hover td { background: #ffffff; }
.ticker { font-weight: 600; color: #58a6ff; }
.ticker { font-weight: 600; color: #0969da; }
.btn-remove {
background: none;
border: none;
color: #484f58;
color: #8c959f;
font-size: 0.875rem;
padding: 0.2rem 0.4rem;
border-radius: 4px;
transition: color 0.15s, background 0.15s;
}
.btn-remove:hover { color: #f85149; background: #1c1c1c; }
.btn-remove:hover { color: #cf222e; background: #eaeef2; }
.empty, .muted { color: #484f58; font-size: 0.875rem; }
.empty, .muted { color: #8c959f; font-size: 0.875rem; }
</style>