Alpaca live paper trade simulation wording; today's top 10 shortlist
Browse files- static/index.html +68 -8
static/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Updraft — live quality-trend fund</title>
|
| 7 |
-
<meta name="description" content="Updraft holds the three quality US stocks with the strongest trends and rebalances itself on the first trading day of every month.
|
| 8 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230e2348'/%3E%3Cpath d='M6 25c6-1 11-6 15-14' stroke='%23ea6435' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3Cpath d='M18.5 9.5 27 6l-3.6 8.4-1.9-3.3z' fill='%23fff'/%3E%3C/svg%3E">
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700&display=swap" rel="stylesheet">
|
|
@@ -77,6 +77,11 @@ h3{font-size:15px;margin:0 0 4px}
|
|
| 77 |
.rank{display:inline-block;min-width:34px;text-align:center;font-size:12px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--chip);color:var(--ink-2)}
|
| 78 |
.note{font-size:13px;color:var(--ink-2);margin:10px 0 0}
|
| 79 |
.empty{color:var(--muted);padding:12px 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:14px}
|
| 81 |
.step{border:1px solid var(--border);border-radius:14px;padding:14px;position:relative;background:var(--surface)}
|
| 82 |
.step .n{font-family:var(--brandfont);font-size:28px;font-weight:700;color:var(--updraft);line-height:1}
|
|
@@ -117,8 +122,8 @@ footer{color:var(--muted);font-size:12px;padding:0 0 36px}
|
|
| 117 |
</div>
|
| 118 |
<div class="wrap hero-copy">
|
| 119 |
<h1>Three quality stocks with the strongest trends — rebalanced automatically every month.</h1>
|
| 120 |
-
<p id="lede">
|
| 121 |
-
S&P 500, and what it holds.</p>
|
| 122 |
</div>
|
| 123 |
<nav class="wrap tabs" role="tablist" aria-label="Sections">
|
| 124 |
<button role="tab" id="t-live" aria-selected="true" aria-controls="p-live">Live fund</button>
|
|
@@ -160,6 +165,14 @@ footer{color:var(--muted);font-size:12px;padding:0 0 36px}
|
|
| 160 |
</tr></thead><tbody><tr><td colspan="6" class="empty">Loading…</td></tr></tbody></table></div>
|
| 161 |
<p class="note" id="cash-note"></p>
|
| 162 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
</section>
|
| 164 |
|
| 165 |
<!-- ===================================================== THE STRATEGY -->
|
|
@@ -272,8 +285,8 @@ function showTab(name) {
|
|
| 272 |
$("t-" + t).setAttribute("aria-selected", String(t === name)); $("p-" + t).hidden = t !== name;
|
| 273 |
}
|
| 274 |
setText($("lede"), name === "strategy"
|
| 275 |
-
? "
|
| 276 |
-
: "
|
| 277 |
if (name === "strategy") loadStrategy();
|
| 278 |
try { history.replaceState(null, "", name === "strategy" ? "#strategy" : "#"); } catch (e) {}
|
| 279 |
}
|
|
@@ -409,7 +422,7 @@ async function loadPortfolio() {
|
|
| 409 |
const p = await get("/api/portfolio"); port = p;
|
| 410 |
if (perf) { syncLive(); showPerf(); }
|
| 411 |
setText($("k-eq"), usd(p.equity));
|
| 412 |
-
showToday();
|
| 413 |
const tb = $("holdings").querySelector("tbody"); tb.textContent = "";
|
| 414 |
if (!p.positions.length) { const tr = tb.insertRow(); const td = tr.insertCell(); td.colSpan = 6; td.className = "empty"; td.textContent = "No positions yet — the first rebalance buys the top 3."; }
|
| 415 |
for (const r of p.positions) {
|
|
@@ -428,6 +441,53 @@ async function loadPortfolio() {
|
|
| 428 |
setText($("updated"), "Live · " + new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); $("dot").style.background = "#3fbf6b";
|
| 429 |
} catch (e) { setText($("updated"), "Live data unavailable"); $("dot").style.background = "#f07068"; }
|
| 430 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
async function loadStatus() {
|
| 432 |
try {
|
| 433 |
const s = await get("/api/status");
|
|
@@ -544,8 +604,8 @@ document.querySelectorAll("#scen button").forEach(b => b.onclick = () => { scen
|
|
| 544 |
function redrawAll() { drawLive(true); if (strat) drawStrategy(); }
|
| 545 |
|
| 546 |
/* ---------- start */
|
| 547 |
-
loadPortfolio(); loadPerformance(); loadStatus();
|
| 548 |
-
setInterval(loadPortfolio, 30000); setInterval(loadStatus, 60000); setInterval(loadPerformance, 60000);
|
| 549 |
if (location.hash === "#strategy") showTab("strategy");
|
| 550 |
</script>
|
| 551 |
</body>
|
|
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Updraft — live quality-trend fund</title>
|
| 7 |
+
<meta name="description" content="Updraft holds the three quality US stocks with the strongest trends and rebalances itself on the first trading day of every month. An Alpaca live paper trade simulation, with the strategy and its backtest.">
|
| 8 |
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%230e2348'/%3E%3Cpath d='M6 25c6-1 11-6 15-14' stroke='%23ea6435' stroke-width='3' fill='none' stroke-linecap='round'/%3E%3Cpath d='M18.5 9.5 27 6l-3.6 8.4-1.9-3.3z' fill='%23fff'/%3E%3C/svg%3E">
|
| 9 |
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700&display=swap" rel="stylesheet">
|
|
|
|
| 77 |
.rank{display:inline-block;min-width:34px;text-align:center;font-size:12px;font-weight:600;padding:2px 8px;border-radius:999px;background:var(--chip);color:var(--ink-2)}
|
| 78 |
.note{font-size:13px;color:var(--ink-2);margin:10px 0 0}
|
| 79 |
.empty{color:var(--muted);padding:12px 0}
|
| 80 |
+
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px;white-space:nowrap;color:var(--ink)}
|
| 81 |
+
.tag.held{background:color-mix(in srgb,var(--updraft) 18%,transparent)}
|
| 82 |
+
.tag.buy{background:color-mix(in srgb,var(--plan) 18%,transparent)}
|
| 83 |
+
.tag.sell{background:var(--chip);color:var(--ink-2)}
|
| 84 |
+
.hold tr.is-held td:first-child{box-shadow:inset 3px 0 0 var(--updraft)}
|
| 85 |
.steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:14px}
|
| 86 |
.step{border:1px solid var(--border);border-radius:14px;padding:14px;position:relative;background:var(--surface)}
|
| 87 |
.step .n{font-family:var(--brandfont);font-size:28px;font-weight:700;color:var(--updraft);line-height:1}
|
|
|
|
| 122 |
</div>
|
| 123 |
<div class="wrap hero-copy">
|
| 124 |
<h1>Three quality stocks with the strongest trends — rebalanced automatically every month.</h1>
|
| 125 |
+
<p id="lede">An Alpaca live paper trade simulation, run by rules, not opinions. Below: what it is worth right now, how it
|
| 126 |
+
compares with the S&P 500, and what it holds.</p>
|
| 127 |
</div>
|
| 128 |
<nav class="wrap tabs" role="tablist" aria-label="Sections">
|
| 129 |
<button role="tab" id="t-live" aria-selected="true" aria-controls="p-live">Live fund</button>
|
|
|
|
| 165 |
</tr></thead><tbody><tr><td colspan="6" class="empty">Loading…</td></tr></tbody></table></div>
|
| 166 |
<p class="note" id="cash-note"></p>
|
| 167 |
</div>
|
| 168 |
+
|
| 169 |
+
<div class="card">
|
| 170 |
+
<div class="chart-head"><div><h2>Today's top 10</h2><p class="sub" id="short-sub">The quality pool ranked by trend — the list the three holdings are picked from.</p></div></div>
|
| 171 |
+
<div style="overflow-x:auto"><table class="hold" id="shortlist"><thead><tr>
|
| 172 |
+
<th class="r">#</th><th>Stock</th><th class="r">6 months</th><th class="r hide-s">1 month</th><th class="r hide-s">Quality</th><th>Status</th>
|
| 173 |
+
</tr></thead><tbody><tr><td colspan="6" class="empty">Loading…</td></tr></tbody></table></div>
|
| 174 |
+
<p class="note" id="short-note"></p>
|
| 175 |
+
</div>
|
| 176 |
</section>
|
| 177 |
|
| 178 |
<!-- ===================================================== THE STRATEGY -->
|
|
|
|
| 285 |
$("t-" + t).setAttribute("aria-selected", String(t === name)); $("p-" + t).hidden = t !== name;
|
| 286 |
}
|
| 287 |
setText($("lede"), name === "strategy"
|
| 288 |
+
? "An Alpaca live paper trade simulation, run by rules, not opinions. Below: how it picks its stocks, 27 years of backtest, and what a lump sum or a monthly SIP could grow to."
|
| 289 |
+
: "An Alpaca live paper trade simulation, run by rules, not opinions. Below: what it is worth right now, how it compares with the S&P 500, and what it holds.");
|
| 290 |
if (name === "strategy") loadStrategy();
|
| 291 |
try { history.replaceState(null, "", name === "strategy" ? "#strategy" : "#"); } catch (e) {}
|
| 292 |
}
|
|
|
|
| 422 |
const p = await get("/api/portfolio"); port = p;
|
| 423 |
if (perf) { syncLive(); showPerf(); }
|
| 424 |
setText($("k-eq"), usd(p.equity));
|
| 425 |
+
showToday(); showShortlist();
|
| 426 |
const tb = $("holdings").querySelector("tbody"); tb.textContent = "";
|
| 427 |
if (!p.positions.length) { const tr = tb.insertRow(); const td = tr.insertCell(); td.colSpan = 6; td.className = "empty"; td.textContent = "No positions yet — the first rebalance buys the top 3."; }
|
| 428 |
for (const r of p.positions) {
|
|
|
|
| 441 |
setText($("updated"), "Live · " + new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); $("dot").style.background = "#3fbf6b";
|
| 442 |
} catch (e) { setText($("updated"), "Live data unavailable"); $("dot").style.background = "#f07068"; }
|
| 443 |
}
|
| 444 |
+
let rk = null;
|
| 445 |
+
async function loadRanking() {
|
| 446 |
+
try { rk = await get("/api/ranking"); showShortlist(); }
|
| 447 |
+
catch (e) { setText($("short-note"), "The ranking is unavailable right now."); }
|
| 448 |
+
}
|
| 449 |
+
function showShortlist() {
|
| 450 |
+
if (!rk || !rk.ranked || !rk.ranked.length) return;
|
| 451 |
+
const N = (rk.rule && rk.rule.names) || 3, K = (rk.rule && rk.rule.keep_within) || 6;
|
| 452 |
+
const order = rk.ranked.map(r => r.ticker), rank = Object.fromEntries(order.map((t, i) => [t, i + 1]));
|
| 453 |
+
const held = port ? port.positions.map(p => p.symbol) : null;
|
| 454 |
+
let target = null; // the same rule as the rebalance: keep holdings in the top K, refill from the top
|
| 455 |
+
if (held) {
|
| 456 |
+
const kept = held.filter(t => (rank[t] || 1e9) <= K).sort((a, b) => rank[a] - rank[b]).slice(0, N);
|
| 457 |
+
target = kept.concat(order.filter(t => !kept.includes(t)).slice(0, N - kept.length));
|
| 458 |
+
}
|
| 459 |
+
const tb = $("shortlist").querySelector("tbody"); tb.textContent = "";
|
| 460 |
+
for (const r of rk.ranked.slice(0, 10)) {
|
| 461 |
+
const isHeld = !!held && held.includes(r.ticker), tr = tb.insertRow();
|
| 462 |
+
if (isHeld) tr.className = "is-held";
|
| 463 |
+
const n = tr.insertCell(); n.className = "r"; const rc = document.createElement("span"); rc.className = "rank"; rc.textContent = "#" + r.rank; n.append(rc);
|
| 464 |
+
const a = tr.insertCell(); const t = document.createElement("div"); t.className = "tick"; t.textContent = r.ticker;
|
| 465 |
+
const co = document.createElement("div"); co.className = "co"; co.textContent = [r.name, r.sector].filter(Boolean).join(" · "); a.append(t, co);
|
| 466 |
+
const m6 = tr.insertCell(); m6.className = "r"; m6.textContent = pct(r.ret_6m, 0); signCls(m6, r.ret_6m);
|
| 467 |
+
const m1 = tr.insertCell(); m1.className = "r hide-s"; m1.textContent = pct(r.ret_1m, 0); signCls(m1, r.ret_1m);
|
| 468 |
+
const q = tr.insertCell(); q.className = "r hide-s"; q.textContent = r.qcd_pct == null ? "—" : "top " + Math.max(1, Math.round((1 - r.qcd_pct) * 100)) + "%";
|
| 469 |
+
const st = tr.insertCell();
|
| 470 |
+
if (target) {
|
| 471 |
+
const tag = document.createElement("span");
|
| 472 |
+
if (isHeld && target.includes(r.ticker)) { tag.className = "tag held"; tag.textContent = "Held"; }
|
| 473 |
+
else if (isHeld) { tag.className = "tag sell"; tag.textContent = "Held · would sell"; }
|
| 474 |
+
else if (target.includes(r.ticker)) { tag.className = "tag buy"; tag.textContent = "Would buy"; }
|
| 475 |
+
if (tag.className) st.append(tag);
|
| 476 |
+
}
|
| 477 |
+
}
|
| 478 |
+
const asOf = rk.prices_as_of ? new Date(rk.prices_as_of + "T12:00:00").toLocaleDateString([], { day: "numeric", month: "short" }) : null;
|
| 479 |
+
setText($("short-sub"), `The ${rk.pool || "~250"} stocks in the quality pool, ranked by trend${asOf ? " on closes of " + asOf : ""} and rebuilt every night. The three holdings are picked from this list at each monthly rebalance.`);
|
| 480 |
+
let note = "";
|
| 481 |
+
if (held && target) {
|
| 482 |
+
const sells = held.filter(x => !target.includes(x)), buys = target.filter(x => !held.includes(x));
|
| 483 |
+
if (!held.length) note = `The first rebalance would buy ${buys.join(", ")}.`;
|
| 484 |
+
else if (!sells.length) note = `If the rebalance ran today: no change — ${held.join(", ")} ${held.length > 1 ? "are all" : "is"} still in the top ${K}.`;
|
| 485 |
+
else note = `If the rebalance ran today: sell ${sells.map(x => x + (rank[x] ? ` (now #${rank[x]})` : " (left the pool)")).join(", ")}; buy ${buys.join(", ")}.`;
|
| 486 |
+
const ahead = order.slice(0, N).filter(x => !held.includes(x) && !buys.includes(x));
|
| 487 |
+
if (ahead.length) note += ` ${ahead.join(", ")} ${ahead.length > 1 ? "rank" : "ranks"} in today's top ${N} but ${ahead.length > 1 ? "are" : "is"} bought only when a holding drops out of the top ${K} — the buffer avoids trading on small rank changes.`;
|
| 488 |
+
}
|
| 489 |
+
setText($("short-note"), (note + ` Quality = percentile on capital discipline among the ${rk.liquid_usd || ""} tradable stocks of the 1,000 largest; the top 30% form the pool.`).replace("among the ", "among the ").trim());
|
| 490 |
+
}
|
| 491 |
async function loadStatus() {
|
| 492 |
try {
|
| 493 |
const s = await get("/api/status");
|
|
|
|
| 604 |
function redrawAll() { drawLive(true); if (strat) drawStrategy(); }
|
| 605 |
|
| 606 |
/* ---------- start */
|
| 607 |
+
loadPortfolio(); loadPerformance(); loadStatus(); loadRanking();
|
| 608 |
+
setInterval(loadPortfolio, 30000); setInterval(loadStatus, 60000); setInterval(loadRanking, 600000); setInterval(loadPerformance, 60000);
|
| 609 |
if (location.hash === "#strategy") showTab("strategy");
|
| 610 |
</script>
|
| 611 |
</body>
|