StockCHat / static /index.html
ronylu's picture
Create static/index.html
5c9e803 verified
Raw
History Blame Contribute Delete
27.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Ahsan Stock Portfolio AI Center</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body{margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial; background:#0b1220; color:#e5e7eb;}
header{padding:16px 18px; border-bottom:1px solid #22324a; background:#0f172a; display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between;}
h1{margin:0; font-size:18px; color:#93c5fd;}
.meta{display:flex; gap:10px; flex-wrap:wrap; font-size:13px; color:#cbd5e1;}
.pill{border:1px solid #22324a; padding:6px 10px; border-radius:999px; background:rgba(2,6,23,.4);}
main{padding:18px; display:grid; grid-template-columns: 1.25fr .75fr; gap:16px; max-width:1700px; margin:0 auto;}
@media(max-width:1100px){main{grid-template-columns:1fr;}}
.card{background:#0f172a; border:1px solid #22324a; border-radius:14px; padding:14px;}
.card h2{margin:0 0 10px; font-size:15px; color:#f1f5f9;}
button{border:0; border-radius:10px; padding:10px 12px; font-weight:700; cursor:pointer; background:#334155; color:#fff;}
button.primary{background:linear-gradient(135deg,#60a5fa,#3b82f6);}
button.good{background:linear-gradient(135deg,#4ade80,#22c55e);}
button.danger{background:linear-gradient(135deg,#fb7185,#ef4444);}
.row{display:flex; gap:10px; flex-wrap:wrap; align-items:center;}
.small{color:#94a3b8; font-size:12px; line-height:1.45;}
table{width:100%; border-collapse:collapse; min-width:1100px;}
th,td{border-bottom:1px solid #22324a; padding:10px; text-align:left; font-size:13px; white-space:nowrap;}
th{position:sticky; top:0; background:#0b1220; z-index:2;}
.wrap{overflow:auto; border:1px solid #22324a; border-radius:12px;}
.pos{color:#4ade80;} .neg{color:#fb7185;} .na{color:#fca5a5;}
.sig{padding:4px 10px; border-radius:999px; border:1px solid #22324a; font-weight:900;}
.BUY{color:#4ade80; background:rgba(34,197,94,.12);}
.SELL{color:#fb7185; background:rgba(239,68,68,.12);}
.HOLD{color:#fbbf24; background:rgba(251,191,36,.12);}
.links a{color:#93c5fd; text-decoration:none; margin-right:10px;}
.links a:hover{text-decoration:underline;}
.list{display:flex; flex-direction:column; gap:10px;}
.item{border:1px solid #22324a; border-radius:12px; padding:10px; background:rgba(2,6,23,.25);}
dialog{border:0; border-radius:14px; background:#0f172a; color:#e5e7eb; width:min(900px,95vw);}
dialog::backdrop{background:rgba(0,0,0,.6);}
.modalHead{display:flex; justify-content:space-between; align-items:center; padding:12px 12px; border-bottom:1px solid #22324a;}
pre{white-space:pre-wrap; background:#0b1220; border:1px solid #22324a; border-radius:12px; padding:12px;}
</style>
</head>
<body>
<header>
<h1>📈 Ahsan Stock Portfolio AI Center</h1>
<div class="meta">
<span class="pill">Time: <b id="now"></b></span>
<span class="pill">Provider: <b id="provider"></b></span>
<span class="pill">AI: <b id="ai"></b></span>
</div>
</header>
<main>
<section class="card">
<h2>Portfolio (live prices, winners/losers, Buy/Sell/Hold)</h2>
<div class="row">
<button class="primary" id="refresh">Refresh Live Prices</button>
<button class="good" id="signals">Compute Buy/Sell/Hold</button>
<button id="export">Export Holdings</button>
<button id="import">Import Holdings</button>
<button class="danger" id="reset">Reset</button>
</div>
<p class="small" style="margin-top:10px;">
To show <b>real prices</b>, add a market data API key in Space secrets:
<span class="pill">FINNHUB_API_KEY</span> or <span class="pill">TWELVEDATA_API_KEY</span>.
For optional AI explanation, add <span class="pill">HF_TOKEN</span>. Signals/forecasts are <b>not financial advice</b>.
</p>
<div class="row" style="margin-top:10px;">
<span class="pill">Market Value: <b id="mv"></b></span>
<span class="pill">Total P/L: <b id="tpnl"></b></span>
<span class="pill">Buy/Hold/Sell: <b id="bhs"></b></span>
</div>
<div class="wrap" style="margin-top:12px;">
<table>
<thead>
<tr>
<th>Ticker</th><th>Company</th><th>Qty</th><th>Live Price</th><th>Market Value</th><th>Cost Basis</th><th>P/L</th><th>P/L%</th><th>Signal</th><th>Links</th><th>Actions</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<p class="small" style="margin-top:10px;">
Canada tickers sometimes need exchange suffixes (example: <b>SHOP.TO</b>). Edit the <b>providerSymbol</b> field via Import.
</p>
</section>
<aside class="card">
<h2>Winners & Losers</h2>
<div class="list" style="margin-top:10px;">
<div class="item"><b>Winners</b><div id="winners" class="small">Refresh + signals to populate.</div></div>
<div class="item"><b>Losers</b><div id="losers" class="small">Refresh + signals to populate.</div></div>
</div>
<h2 style="margin-top:14px;">News & Chart</h2>
<div class="item small">Open any ticker → <b>Analyze</b> for chart/forecast and optional AI. Use <b>News</b> to fetch latest articles (Finnhub) and click to open.</div>
<div class="item">
<canvas id="chart" height="220"></canvas>
<div id="chartMeta" class="small" style="margin-top:8px;"></div>
</div>
<div class="item">
<b>News</b>
<div id="news" class="small">Pick a ticker → News.</div>
</div>
</aside>
</main>
<dialog id="modal">
<div class="modalHead">
<div class="row">
<span class="pill">Ticker: <b id="mSym"></b></span>
<span class="pill">Signal: <b id="mSig"></b></span>
<span class="pill">5d exp: <b id="mExp"></b></span>
</div>
<button id="close">Close</button>
</div>
<div style="padding:12px;">
<div class="row">
<button class="good" id="aiBtn">AI Explain</button>
<button id="newsBtn">News</button>
<button id="fundBtn">Fundamentals</button>
</div>
<p class="small" id="reasons" style="margin-top:10px;"></p>
<pre id="aiOut" class="small"></pre>
<div id="fundOut" class="small"></div>
<div id="links" class="links" style="margin-top:10px;"></div>
</div>
</dialog>
<input type="file" id="file" accept="application/json" style="display:none"/>
<script>
const DEFAULT_HOLDINGS = [{"symbol": "ACRY", "name": "Actwon Therapeutics Inc.", "quantity": 20, "currency": "USD", "country": "USA", "sector": "Healthcare", "providerSymbol": "ACRY", "lastValue": 36.4, "lastReturn": 5.4}, {"symbol": "ADAPY", "name": "Adaptimmune Therapeutics PLC", "quantity": 2000, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "ADAPY", "lastValue": 60.8, "lastReturn": -79.2}, {"symbol": "ADIL", "name": "Adial Pharmaceuticals Inc.", "quantity": 500, "currency": "USD", "country": "USA", "sector": "Pharmaceuticals", "providerSymbol": "ADIL", "lastValue": 125.0, "lastReturn": -90.35}, {"symbol": "ADTX", "name": "Adrixx Inc.", "quantity": 162478, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "ADTX", "lastValue": 123636.5, "lastReturn": -574.74}, {"symbol": "AEMD", "name": "Aethlon Medical Inc.", "quantity": 17.5, "currency": "USD", "country": "USA", "sector": "Medical Devices", "providerSymbol": "AEMD", "lastValue": 41.48, "lastReturn": -92.78}, {"symbol": "AREB", "name": "American Rebel Holdings Inc.", "quantity": 50, "currency": "USD", "country": "USA", "sector": "Consumer Goods", "providerSymbol": "AREB", "lastValue": 34.18, "lastReturn": -46.32}, {"symbol": "ATOS", "name": "Atossa Therapeutics Inc.", "quantity": 210, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "ATOS", "lastValue": 136.5, "lastReturn": -37.67}, {"symbol": "ATYR", "name": "aTyr Pharma Inc.", "quantity": 200, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "ATYR", "lastValue": 148.22, "lastReturn": -3.78}, {"symbol": "AZI", "name": "Aurazi Internet Technology Global Limited", "quantity": 64.02, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "AZI", "lastValue": 194.62, "lastReturn": -87.67}, {"symbol": "BABY", "name": "Else Nutrition Holdings Inc.", "quantity": 98, "currency": "CAD", "country": "Canada", "sector": "Consumer Goods", "providerSymbol": "BABY.TO", "lastValue": 9.31, "lastReturn": -10.29}, {"symbol": "BNN", "name": "Bollinger Innovations Inc.", "quantity": 2, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "BNN", "lastValue": 0.21, "lastReturn": -480.05}, {"symbol": "BIYA", "name": "Unknown", "quantity": 4, "currency": "USD", "country": "USA", "sector": "Unknown", "providerSymbol": "BIYA", "lastValue": 15.8, "lastReturn": -15.31}, {"symbol": "BYL", "name": "Baylin Technologies Inc.", "quantity": 500, "currency": "CAD", "country": "Canada", "sector": "Technology", "providerSymbol": "BYL.TO", "lastValue": 140.0, "lastReturn": 0.0}, {"symbol": "CANF", "name": "Can-Fite BioPharma Ltd", "quantity": 27.5, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "CANF", "lastValue": 110.83, "lastReturn": -169.66}, {"symbol": "DB", "name": "Cannabis (DB)", "quantity": 2000, "currency": "CAD", "country": "Canada", "sector": "Cannabis", "providerSymbol": "DB.TO", "lastValue": 220.0, "lastReturn": 65.0}, {"symbol": "DOW", "name": "Dow Inc.", "quantity": 1, "currency": "USD", "country": "USA", "sector": "Chemicals", "providerSymbol": "DOW", "lastValue": 26.18, "lastReturn": 10.01}, {"symbol": "GAME", "name": "GameSquare Holdings Inc.", "quantity": 80, "currency": "USD", "country": "USA", "sector": "Entertainment", "providerSymbol": "GAME", "lastValue": 39.83, "lastReturn": -20.98}, {"symbol": "HBIO", "name": "Harvard Bioscience Inc.", "quantity": 50, "currency": "USD", "country": "USA", "sector": "Healthcare", "providerSymbol": "HBIO", "lastValue": 33.5, "lastReturn": 10.96}, {"symbol": "ILLM", "name": "Illumin Holdings Inc.", "quantity": 150, "currency": "CAD", "country": "Canada", "sector": "Technology", "providerSymbol": "ILLM.TO", "lastValue": 162.75, "lastReturn": -38.75}, {"symbol": "IMUX", "name": "Immunic Inc", "quantity": 350, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "IMUX", "lastValue": 238.53, "lastReturn": -44.66}, {"symbol": "IRWD", "name": "Ironwood Pharmaceuticals Inc.", "quantity": 1, "currency": "USD", "country": "USA", "sector": "Pharmaceuticals", "providerSymbol": "IRWD", "lastValue": 4.09, "lastReturn": 3.45}, {"symbol": "JOB", "name": "GEE Group Inc", "quantity": 1001, "currency": "USD", "country": "USA", "sector": "Services", "providerSymbol": "JOB", "lastValue": 205.61, "lastReturn": -15.01}, {"symbol": "JZXN", "name": "Jiuzi Holdings Inc.", "quantity": 12.5, "currency": "USD", "country": "USA", "sector": "Unknown", "providerSymbol": "JZXN", "lastValue": 23.75, "lastReturn": -109.75}, {"symbol": "KUST", "name": "Kustom Entertainment Inc.", "quantity": 40.6668, "currency": "USD", "country": "USA", "sector": "Entertainment", "providerSymbol": "KUST", "lastValue": 102.89, "lastReturn": -1237.49}, {"symbol": "LXEH", "name": "Lixiang Education Holding Co Ltd", "quantity": 138, "currency": "USD", "country": "USA", "sector": "Education", "providerSymbol": "LXEH", "lastValue": 28.4, "lastReturn": -99.15}, {"symbol": "MLGO", "name": "MicroAlgo Inc.", "quantity": 1, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "MLGO", "lastValue": 5.38, "lastReturn": -2216.44}, {"symbol": "MODD", "name": "Modular Medical Inc.", "quantity": 550, "currency": "USD", "country": "USA", "sector": "Medical Devices", "providerSymbol": "MODD", "lastValue": 288.53, "lastReturn": -36.56}, {"symbol": "MRMD", "name": "MariMed Inc.", "quantity": 1000, "currency": "CAD", "country": "Canada", "sector": "Cannabis", "providerSymbol": "MRMD.TO", "lastValue": 110.0, "lastReturn": -45.0}, {"symbol": "NCNA", "name": "NuCana plc", "quantity": 29, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "NCNA", "lastValue": 102.37, "lastReturn": -150.07}, {"symbol": "NEVI", "name": "Nevia Brands Inc.", "quantity": 1000, "currency": "CAD", "country": "Canada", "sector": "Consumer Goods", "providerSymbol": "NEVI.TO", "lastValue": 50.0, "lastReturn": 0.0}, {"symbol": "NUWE", "name": "Nuwellis Inc.", "quantity": 50, "currency": "USD", "country": "USA", "sector": "Unknown", "providerSymbol": "NUWE", "lastValue": 104.0, "lastReturn": -195.5}, {"symbol": "OCEA", "name": "Ocean Biomedical Inc.", "quantity": 10000, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "OCEA", "lastValue": 4.0, "lastReturn": -424.5}, {"symbol": "OGEN", "name": "Oragenics, Inc.", "quantity": 0.0333, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "OGEN", "lastValue": 0.0285, "lastReturn": -0.11}, {"symbol": "PMN", "name": "ProMIS Neurosciences Inc.", "quantity": 13.76, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "PMN", "lastValue": 115.17, "lastReturn": -335.08}, {"symbol": "PPR", "name": "Prairie Provident Resources Inc.", "quantity": 223.3333, "currency": "CAD", "country": "Canada", "sector": "Energy", "providerSymbol": "PPR.TO", "lastValue": 145.17, "lastReturn": -355.83}, {"symbol": "PSTV", "name": "Plus Therapeutics Inc.", "quantity": 100, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "PSTV", "lastValue": 50.0, "lastReturn": 6.8}, {"symbol": "PYR", "name": "PyroGenesis Canada Inc.", "quantity": 3200, "currency": "CAD", "country": "Canada", "sector": "Technology", "providerSymbol": "PYR.TO", "lastValue": 928.0, "lastReturn": -323.99}, {"symbol": "SDS", "name": "ProShares UltraShort S&P500", "quantity": 2, "currency": "USD", "country": "USA", "sector": "ETF", "providerSymbol": "SDS", "lastValue": 137.76, "lastReturn": -328.9}, {"symbol": "SENS", "name": "Senseonics Holdings, Inc.", "quantity": 50, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "SENS", "lastValue": 368.0, "lastReturn": -82.05}, {"symbol": "SGMO", "name": "Sangamo Therapeutics Inc.", "quantity": 150, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "SGMO", "lastValue": 66.68, "lastReturn": -19.08}, {"symbol": "SOXS", "name": "Direxion Daily Semiconductor Bear 3X Shares", "quantity": 101.6614, "currency": "USD", "country": "USA", "sector": "ETF", "providerSymbol": "SOXS", "lastValue": 250.09, "lastReturn": -328.69}, {"symbol": "TLRY", "name": "Tilray Brands, Inc.", "quantity": 0.1, "currency": "USD", "country": "USA", "sector": "Cannabis", "providerSymbol": "TLRY", "lastValue": 0.896, "lastReturn": 0.41}, {"symbol": "VRAX", "name": "Vrax Biolabs Group Ltd", "quantity": 300, "currency": "USD", "country": "USA", "sector": "Biotechnology", "providerSymbol": "VRAX", "lastValue": 108.6, "lastReturn": -64.33}, {"symbol": "WATT", "name": "Energous Corporation", "quantity": 0.0333, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "WATT", "lastValue": 0.17, "lastReturn": -0.1}, {"symbol": "WDH", "name": "Waterdrop Inc.", "quantity": 101.4088, "currency": "USD", "country": "USA", "sector": "Financial Services", "providerSymbol": "WDH", "lastValue": 188.62, "lastReturn": -175.0}, {"symbol": "WKHS", "name": "Workhorse Group, Inc.", "quantity": 8.3333, "currency": "USD", "country": "USA", "sector": "Automotive", "providerSymbol": "WKHS", "lastValue": 45.25, "lastReturn": -32.65}, {"symbol": "ZSPC", "name": "zSpace, Inc.", "quantity": 298, "currency": "USD", "country": "USA", "sector": "Technology", "providerSymbol": "ZSPC", "lastValue": 164.58, "lastReturn": -121.02}];
const LS_KEY = "ahsan_holdings_v3";
let holdings = load();
const el = (id)=>document.getElementById(id);
const fmt = (x,cur)=> (x==null || isNaN(x)) ? "—" : x.toLocaleString(undefined,{style:"currency",currency:cur||"USD"});
const fmtPct = (x)=> (x==null || isNaN(x)) ? "—" : x.toFixed(2)+"%";
const cls = (x)=> (x==null||isNaN(x)) ? "" : (x>=0 ? "pos" : "neg");
const LINKS = {
Yahoo: s => `https://finance.yahoo.com/quote/${encodeURIComponent(s)}`,
StockAnalysis: s => `https://stockanalysis.com/stocks/${encodeURIComponent(s.replace(/\..*$/,''))}/`,
Finviz: s => `https://finviz.com/quote.ashx?t=${encodeURIComponent(s.replace(/\..*$/,''))}`,
TradingView: s => `https://www.tradingview.com/symbols/${encodeURIComponent(s.replace(/\..*$/,''))}/`,
EDGAR: s => `https://www.sec.gov/edgar/search/#/q=${encodeURIComponent(s.replace(/\..*$/,''))}`,
TMX: s => `https://money.tmx.com/en/quote/${encodeURIComponent(s.replace(/\..*$/,''))}`,
SEDAR: _ => `https://www.sedarplus.ca/csa-public/search`,
};
function load(){
try{
const raw = localStorage.getItem(LS_KEY);
if(raw) return JSON.parse(raw);
}catch(e){}
localStorage.setItem(LS_KEY, JSON.stringify(DEFAULT_HOLDINGS));
return JSON.parse(JSON.stringify(DEFAULT_HOLDINGS));
}
function save(){ localStorage.setItem(LS_KEY, JSON.stringify(holdings)); }
function costBasis(h){
if(h.costBasis!=null) return h.costBasis;
if(typeof h.lastValue==="number" && typeof h.lastReturn==="number") return (h.lastValue - h.lastReturn);
return null;
}
async function post(path, body){
const r = await fetch(path,{method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(body)});
if(!r.ok) throw new Error(await r.text());
return r.json();
}
function now(){ el("now").textContent = new Date().toLocaleString(); }
setInterval(now, 1000); now();
let chart;
function initChart(){
const ctx = el("chart").getContext("2d");
chart = new Chart(ctx, {
type:"line",
data:{labels:[], datasets:[{label:"Close", data:[]}, {label:"Forecast", data:[]}]},
options:{responsive:true, plugins:{legend:{labels:{color:"#cbd5e1"}}},
scales:{x:{ticks:{color:"#94a3b8"}}, y:{ticks:{color:"#94a3b8"}}}}
});
}
function linksHTML(sym, country){
const isCA = (country||"").toLowerCase().includes("canada") || /\.(TO|V|CN)$/i.test(sym);
const a = (n,u)=>`<a target="_blank" rel="noreferrer" href="${u}">${n}</a>`;
return [
a("Yahoo", LINKS.Yahoo(sym)),
a("StockAnalysis", LINKS.StockAnalysis(sym)),
a("Finviz", LINKS.Finviz(sym)),
a("TradingView", LINKS.TradingView(sym)),
isCA ? a("TMX", LINKS.TMX(sym)) : a("EDGAR", LINKS.EDGAR(sym)),
isCA ? a("SEDAR+", LINKS.SEDAR(sym)) : ""
].join("");
}
function render(rows){
const tb = el("tbody"); tb.innerHTML="";
let mv=0, pnl=0, buy=0, hold=0, sell=0;
for(const r of rows){
const h = r.__h;
const cur = h.currency || "USD";
if(r.marketValue!=null) mv += r.marketValue;
if(r.pnl!=null) pnl += r.pnl;
const s = r.signal?.label;
if(s==="BUY") buy++; else if(s==="SELL") sell++; else if(s==="HOLD") hold++;
const tr = document.createElement("tr");
const priceCell = (r.quote?.price==null) ? `<span class="na">N/A</span>` : fmt(r.quote.price, cur);
tr.innerHTML = `
<td><b>${h.symbol}</b><div class="small">${h.country}${h.sector||"—"}<br/>Provider: ${h.providerSymbol||h.symbol}</div></td>
<td>${h.name||"—"}</td>
<td>${Number(h.quantity||0).toLocaleString()}</td>
<td>${priceCell}</td>
<td>${r.marketValue==null?"—":fmt(r.marketValue, cur)}</td>
<td>${r.costBasis==null?'<span class="na">Missing</span>':fmt(r.costBasis, cur)}</td>
<td class="${cls(r.pnl)}">${r.pnl==null?"—":fmt(r.pnl, cur)}</td>
<td class="${cls(r.pnlPct)}">${r.pnlPct==null?"—":fmtPct(r.pnlPct)}</td>
<td>${s?`<span class="sig ${s}">${s}</span>`:"—"}</td>
<td class="links">${linksHTML(h.providerSymbol||h.symbol, h.country)}</td>
<td>
<button data-act="an" data-sym="${encodeURIComponent(h.providerSymbol||h.symbol)}">Analyze</button>
<button data-act="news" data-sym="${encodeURIComponent(h.providerSymbol||h.symbol)}">News</button>
</td>
`;
tb.appendChild(tr);
}
el("mv").textContent = fmt(mv,"USD");
el("tpnl").textContent = fmt(pnl,"USD");
el("tpnl").className = pnl>=0 ? "pos" : "neg";
el("bhs").textContent = `${buy} / ${hold} / ${sell}`;
tb.querySelectorAll("button[data-act]").forEach(btn=>{
btn.addEventListener("click", ()=>{
const act = btn.getAttribute("data-act");
const sym = decodeURIComponent(btn.getAttribute("data-sym"));
if(act==="an") openModal(sym);
if(act==="news") loadNews(sym);
});
});
}
async function health(){
const r = await fetch("/api/health"); const j = await r.json();
el("provider").textContent = j.market_data_provider;
el("ai").textContent = j.ai_enabled ? `enabled (${j.hf_model})` : "disabled";
}
async function refresh(){
const symbols = holdings.map(h=>h.providerSymbol||h.symbol);
const q = await post("/api/quotes",{symbols});
const map = new Map(q.quotes.map(x=>[x.symbol,x]));
const rows = holdings.map(h=>{
const sym = h.providerSymbol||h.symbol;
const quote = map.get(sym) || {symbol:sym, price:null};
const cb = costBasis(h);
const mv = (quote.price!=null) ? Number(quote.price)*Number(h.quantity||0) : null;
const pl = (mv!=null && cb!=null) ? mv - cb : null;
const plp = (pl!=null && cb && cb!==0) ? (pl/cb*100) : null;
return {__h:h, quote, costBasis:cb, marketValue:mv, pnl:pl, pnlPct:plp, signal:{label:null}};
});
render(rows);
}
async function signals(){
const payload = holdings.map(h=>({
symbol:h.symbol, providerSymbol:h.providerSymbol||h.symbol, quantity:h.quantity,
currency:h.currency, country:h.country, sector:h.sector, costBasis:costBasis(h),
lastValue:h.lastValue, lastReturn:h.lastReturn
}));
const res = await post("/api/portfolio/analyze",{holdings:payload});
const byPS = new Map(res.results.map(x=>[x.providerSymbol,x]));
const rows = holdings.map(h=>{
const ps = h.providerSymbol||h.symbol;
const r = byPS.get(ps) || {};
r.__h = h;
return r;
});
render(rows);
el("winners").innerHTML = (res.topWinners||[]).map(x=>`<div><b>${x.providerSymbol||x.symbol}</b> <span class="pos">${fmtPct(x.pnlPct)}</span></div>`).join("") || "—";
el("losers").innerHTML = (res.topLosers||[]).map(x=>`<div><b>${x.providerSymbol||x.symbol}</b> <span class="neg">${fmtPct(x.pnlPct)}</span></div>`).join("") || "—";
}
const modal = el("modal");
el("close").onclick = ()=>modal.close();
let currentSym = null;
async function openModal(sym){
currentSym = sym;
el("mSym").textContent = sym;
el("aiOut").textContent = "";
el("fundOut").innerHTML = "";
el("reasons").textContent = "Loading…";
const holding = holdings.find(h=>(h.providerSymbol||h.symbol)===sym) || {};
el("links").innerHTML = linksHTML(sym, holding.country);
modal.showModal();
const j = await post("/api/analyze",{symbol:sym, include_ai:false});
el("mSig").textContent = j.signal?.label || "—";
el("mExp").textContent = j.expected_return_pct_5d==null ? "—" : (j.expected_return_pct_5d.toFixed(2)+"%");
el("reasons").textContent = (j.signal?.reasons||[]).join(" • ") || "—";
const closes = j.closes || [];
const fc = (j.forecast?.forecast||[]);
const labels = closes.map((_,i)=>String(i-closes.length+1)).concat(fc.map((_,i)=>"F"+(i+1)));
const dataClose = closes.concat(new Array(fc.length).fill(null));
const dataFc = new Array(Math.max(0,closes.length-1)).fill(null).concat([closes[closes.length-1] || null]).concat(fc);
chart.data.labels = labels;
chart.data.datasets[0].data = dataClose;
chart.data.datasets[1].data = dataFc;
chart.update();
el("chartMeta").textContent = `${sym} • Signal: ${j.signal?.label} • Forecast: ${j.forecast?.method}`;
}
el("aiBtn").onclick = async ()=>{
if(!currentSym) return;
el("aiOut").textContent = "Generating AI…";
const j = await post("/api/analyze",{symbol:currentSym, include_ai:true});
el("aiOut").textContent = j.ai || "(no output – set HF_TOKEN)";
};
async function loadNews(sym){
el("news").textContent = "Loading…";
try{
const j = await post("/api/news",{symbol:sym, days:7});
const a = j.articles || [];
if(!a.length){
el("news").innerHTML = `No API news (or Finnhub not set). Use links: ${linksHTML(sym,"")}`;
return;
}
el("news").innerHTML = a.slice(0,8).map(x=>`<div>• <a target="_blank" rel="noreferrer" href="${x.url}">${x.headline}</a> <span class="small">(${x.source||""})</span></div>`).join("");
}catch(e){
el("news").textContent = "News error: "+e.message;
}
}
el("newsBtn").onclick = ()=> currentSym && loadNews(currentSym);
el("fundBtn").onclick = async ()=>{
if(!currentSym) return;
el("fundOut").innerHTML = "<div class='small'>Loading fundamentals…</div>";
const j = await post("/api/fundamentals",{symbol:currentSym});
if(!j.profile || Object.keys(j.profile).length===0){
el("fundOut").innerHTML = "<div class='small'>No fundamentals from API (needs FINNHUB_API_KEY). Use links above.</div>";
return;
}
const p = j.profile || {};
const m = (j.metrics||{}).metric || {};
el("fundOut").innerHTML = `
<div class="small"><b>${p.name||""}</b> • Exchange: ${p.exchange||""} • Industry: ${p.finnhubIndustry||""}</div>
<div class="small">Market Cap: ${p.marketCapitalization||"—"} • P/E (TTM): ${m.peTTM ?? "—"} • EPS (TTM): ${m.epsTTM ?? "—"}</div>
<div class="small">Open StockAnalysis/Yahoo links for full financial statements.</div>
`;
};
el("export").onclick = ()=>{
const blob = new Blob([JSON.stringify(holdings,null,2)], {type:"application/json"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.download = "ahsan_holdings.json"; a.click();
URL.revokeObjectURL(a.href);
};
el("import").onclick = ()=>el("file").click();
el("file").onchange = async (e)=>{
const f = e.target.files?.[0]; if(!f) return;
const txt = await f.text();
try{
const j = JSON.parse(txt);
if(!Array.isArray(j)) throw new Error("JSON must be an array");
holdings = j; save();
await refresh();
}catch(err){ alert("Import failed: "+err.message); }
};
el("reset").onclick = async ()=>{ holdings = JSON.parse(JSON.stringify(DEFAULT_HOLDINGS)); save(); await refresh(); };
el("refresh").onclick = refresh;
el("signals").onclick = signals;
window.addEventListener("DOMContentLoaded", async ()=>{
initChart();
await health();
await refresh();
});
</script>
</body>
</html>