s-ttp commited on
Commit
a64edcf
·
verified ·
1 Parent(s): c788d9a

Alpaca live paper trade simulation wording; today's top 10 shortlist

Browse files
Files changed (1) hide show
  1. 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. Live paper portfolio, strategy and 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,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">A live paper portfolio run by rules, not opinions. Below: what it is worth right now, how it compares with the
121
- S&amp;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
- ? "A live paper portfolio 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."
276
- : "A live paper portfolio run by rules, not opinions. Below: what it is worth right now, how it compares with the S&P 500, and what it holds.");
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&amp;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>