| <!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> |
|
|