mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 08:44:43 +02:00
feat(finlab): boutons d'analyse rapide (digest/analyse/plan/fondamental/opport./alertes) (#70)
Panneau « Actions rapides » sur le dashboard : 6 boutons one-click qui renvoient une analyse finlab instantanée (déterministe, gratuite, sans LLM), dans une modale. - 📊 Analyse (action sélectionnée) : technique (RSI/MACD/MM/signaux) + fondamental (PER/PEG/ROE/marges/dette/reco/cible) + plan R:R, combinés en une fiche - 📐 Plan R:R, 📈 Fondamentaux (action sélectionnée) ; 📋 Digest, 🔭 Opportunités, 🔔 Alertes (global) - Backend : endpoints /api/digest, /api/fundamentals, /api/analyze - Fix : fundamental.compare_portfolio() utilisait l'ancien schéma mono-compte (pf["positions"]) → cassé par le multi-comptes ; passe par data.portfolio_tickers() Vérifié : endpoints OK (TestClient), rendu modale au navigateur (Playwright) — fiche Analyse NVDA complète (technique + fondamental + plan). JS node --check OK. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b09d3db1fa
commit
73e2ad30db
3 changed files with 122 additions and 3 deletions
|
|
@ -37,6 +37,12 @@
|
|||
.atab:hover{border-color:var(--acc);color:#fff}
|
||||
.atab.on{background:var(--acc);border-color:var(--acc);color:#fff}
|
||||
.atag{font-size:11px;opacity:.85}
|
||||
.qa-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
||||
.btn.qa{text-align:left;padding:8px 10px;font-size:12.5px}
|
||||
#resBody table{width:100%}
|
||||
#resBody pre{background:var(--bg);padding:12px;border-radius:8px;white-space:pre-wrap;font-size:12.5px}
|
||||
#resBody .kv{display:flex;justify-content:space-between;padding:4px 6px;border-bottom:1px solid var(--line);font-size:13px}
|
||||
#resBody .kv b{color:#fff}
|
||||
.row .t{font-weight:600;color:#fff}
|
||||
.row .s{font-size:12px;color:var(--mut)}
|
||||
.mono{font-variant-numeric:tabular-nums}
|
||||
|
|
@ -154,8 +160,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite : alertes -->
|
||||
<!-- Colonne droite : actions rapides + alertes -->
|
||||
<div>
|
||||
<div class="panel">
|
||||
<h3>Actions rapides <span class="muted" id="qaTicker" style="text-transform:none;letter-spacing:0"></span></h3>
|
||||
<div class="qa-grid">
|
||||
<button class="btn qa" onclick="qaDigest()">📋 Digest</button>
|
||||
<button class="btn qa" onclick="qaAnalyze()">📊 Analyse</button>
|
||||
<button class="btn qa" onclick="qaPlanQuick()">📐 Plan R:R</button>
|
||||
<button class="btn qa" onclick="qaFundamentals()">📈 Fondamentaux</button>
|
||||
<button class="btn qa" onclick="qaOpportunities()">🔭 Opportunités</button>
|
||||
<button class="btn qa" onclick="qaAlerts()">🔔 Alertes</button>
|
||||
</div>
|
||||
<div class="muted" style="font-size:11px;margin-top:7px">Analyse / Plan / Fondamentaux → sur l'action <b>sélectionnée</b> (clique une ligne ou un titre). Le reste = global.</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h3>Alertes du jour <select id="alertWatch" style="font-size:11px" onchange="loadAlerts()">
|
||||
<option value="all">tous</option><option value="portfolio">portefeuille</option></select></h3>
|
||||
|
|
@ -204,6 +222,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal résultats (actions rapides) -->
|
||||
<div class="modal" id="resModal" onclick="if(event.target===this)this.classList.remove('on')">
|
||||
<div class="box" style="min-width:520px;max-width:760px">
|
||||
<h3 style="margin-bottom:10px" id="resTitle">Résultat</h3>
|
||||
<div id="resBody" style="max-height:70vh;overflow:auto">—</div>
|
||||
<div class="controls" style="margin-top:12px">
|
||||
<button class="btn" onclick="document.getElementById('resModal').classList.remove('on')">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let CHART, S = {}, SEL=null, PERIOD='6mo', CCY='$';
|
||||
const fmt = (n,d=0)=> n==null? '—' : Number(n).toLocaleString('fr-FR',{minimumFractionDigits:d,maximumFractionDigits:d});
|
||||
|
|
@ -242,6 +271,7 @@ async function loadChart(ticker){
|
|||
document.querySelectorAll('.row.sel,tr.sel').forEach(e=>e.classList.remove('sel'));
|
||||
document.querySelectorAll('[data-tk="'+ticker+'"]').forEach(e=>e.classList.add('sel'));
|
||||
document.getElementById('chartTitle').textContent = ticker + ' · ' + PERIOD;
|
||||
document.getElementById('qaTicker').textContent = '· ' + ticker;
|
||||
document.getElementById('techChips').innerHTML='<span class="loading">Chargement…</span>';
|
||||
document.getElementById('techSignals').textContent='';
|
||||
try{
|
||||
|
|
@ -418,6 +448,62 @@ async function uploadImport(f){
|
|||
finally{ inp.value=''; }
|
||||
}
|
||||
|
||||
// ── Actions rapides (finlab pur, one-click) ────────────────
|
||||
const escapeHtml = s => (s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
|
||||
function showRes(title, html){
|
||||
document.getElementById('resTitle').innerHTML=title;
|
||||
document.getElementById('resBody').innerHTML=html;
|
||||
document.getElementById('resModal').classList.add('on');
|
||||
}
|
||||
function needTicker(){ if(!SEL){ alert("Sélectionne d'abord une action (clique une ligne ou un titre)."); return null; } return SEL; }
|
||||
async function qaRun(title, fn){ showRes(title,'<div class="loading">Calcul…</div>'); try{ await fn(); }catch(e){ document.getElementById('resBody').innerHTML='<div class="err">'+e.message+'</div>'; } }
|
||||
const setBody = h => document.getElementById('resBody').innerHTML = h;
|
||||
|
||||
function qaDigest(){ qaRun('📋 Digest', async()=>{ const d=await api('/api/digest'); setBody('<pre>'+escapeHtml(d.text)+'</pre>'); }); }
|
||||
function qaPlanQuick(){ const t=needTicker(); if(!t)return; qaRun('📐 Plan R:R — '+t, async()=>{ const d=await api('/api/plan?ticker='+t); setBody('<pre>'+escapeHtml(d.render)+'</pre>'); }); }
|
||||
function qaOpportunities(){ qaRun('🔭 Opportunités (haussières)', async()=>{ const d=await api('/api/scan?theme=all&bullish=true'); setBody(scanTable(d.rows)); }); }
|
||||
function qaAlerts(){ qaRun('🔔 Alertes du jour', async()=>{ const d=await api('/api/alerts?watch=all'); setBody(alertsHtml(d.hits)); }); }
|
||||
function qaFundamentals(){ const t=needTicker(); if(!t)return; qaRun('📈 Fondamentaux — '+t, async()=>{ const d=await api('/api/fundamentals?ticker='+t); setBody(fundHtml(d.snapshot)); }); }
|
||||
function qaAnalyze(){ const t=needTicker(); if(!t)return; qaRun('📊 Analyse — '+t, async()=>{ const d=await api('/api/analyze?ticker='+t); setBody(analyzeHtml(d)); }); }
|
||||
|
||||
const kvRow = (k,v) => `<div class="kv"><span class="muted">${k}</span><b>${v==null?'—':v}</b></div>`;
|
||||
function fundHtml(f){
|
||||
if(!f||f.error) return '<div class="err">'+((f&&f.error)||'indisponible')+'</div>';
|
||||
return [['Nom',f.nom],['Secteur',f.secteur],['Cours',f.cours],['PER',f.PER],['PER fwd',f.PER_fwd],
|
||||
['PEG',f.PEG],['P/S',f['P/S']],['P/B',f['P/B']],['Marge nette %',f['marge_nette_%']],['ROE %',f['ROE_%']],
|
||||
['Croiss. CA %',f['croiss_CA_%']],['Dette/FP',f['dette/FP']],['Div %',f['div_%']],['Capi',f.capi],
|
||||
['Reco analystes',f.reco],['Cible moy.',f.cible_moy]].map(([k,v])=>kvRow(k,v)).join('');
|
||||
}
|
||||
function techHtml(t){
|
||||
if(!t||t.error) return '<div class="err">'+((t&&t.error)||'indisponible')+'</div>';
|
||||
const mc=t.MACD==='haussier'?'pos':'neg', rc=t.RSI14>=70?'neg':(t.RSI14<=30?'pos':'');
|
||||
return `<div class="chiprow">
|
||||
<span class="chip">Cours <b>${t.cours}</b></span><span class="chip">RSI <b class="${rc}">${t.RSI14}</b></span>
|
||||
<span class="chip">MACD <b class="${mc}">${t.MACD}</b></span><span class="chip">MM50 <b>${t.MM50}</b></span>
|
||||
<span class="chip">MM200 <b>${t.MM200??'—'}</b></span><span class="chip">Volatilité <b>${t['vol_%']}%</b></span></div>
|
||||
<div class="signals">▸ ${(t.signaux||[]).join(' · ')}</div>`;
|
||||
}
|
||||
function analyzeHtml(d){
|
||||
return `<h4 style="margin:4px 0 8px">Technique</h4>${techHtml(d.technical)}
|
||||
<h4 style="margin:14px 0 6px">Fondamental</h4>${fundHtml(d.fundamental)}
|
||||
<h4 style="margin:14px 0 6px">Plan R:R</h4>${d.plan_render?'<pre>'+escapeHtml(d.plan_render)+'</pre>':'<div class="muted">—</div>'}`;
|
||||
}
|
||||
function scanTable(rows){
|
||||
if(!rows||!rows.length) return '<div class="muted">Aucun résultat.</div>';
|
||||
return '<table><thead><tr><th>Tkr</th><th>Cours</th><th>Sem%</th><th>RSI</th><th>M</th><th>Biais</th></tr></thead><tbody>'
|
||||
+ rows.map(r=>`<tr style="cursor:pointer" onclick="loadChart('${r.ticker}');document.getElementById('resModal').classList.remove('on')">
|
||||
<td class="t">${r.ticker}</td><td class="mono">${r.cours??'—'}</td><td class="mono">${r['range_sem_%']??'—'}</td>
|
||||
<td class="mono">${r.RSI??'—'}</td><td class="${r.MACD==='↑'?'pos':'neg'}">${r.MACD??''}</td>
|
||||
<td style="text-align:left;font-size:11px">${biasFmt(r.biais)}</td></tr>`).join('')+'</tbody></table>';
|
||||
}
|
||||
function alertsHtml(hits){
|
||||
if(!hits||!hits.length) return '<div class="muted">Aucune alerte aujourd\'hui.</div>';
|
||||
return hits.map(h=>{ const up=/hausse|survente|rebond|haussier|golden/i.test(h.alerte);
|
||||
return `<div class="alert"><div class="dot" style="background:${up?'var(--up)':'var(--down)'}"></div>
|
||||
<div style="flex:1"><b>${h.ticker}</b> <span class="muted">${h.alerte}</span></div><span class="mono">${h.prix}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Chaîne de valeur IA (couches) ──────────────────────────
|
||||
const LAYER_META = {
|
||||
energy_power: {ic:'⚡', lb:'Énergie', ds:'alimente les datacenters'},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue