尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Dollar Cost Averaging (DCA) is an investment strategy where an investor divides up the total amount to be invested across periodic purchases of a target asset, to reduce the impact of volatility on the overall purchase. It is also how most people already invest in the stock market: a 401(k) takes a fixed contribution from every paycheck, often topped up by an employer match, and buys the same fund on the same schedule whatever the price.檢視完整 README ↗Dollar Cost Averaging (DCA) is an investment strategy where an investor divides up the total amount to be invested across periodic purchases of a target asset, to reduce the impact of volatility on the overall purchase. It is also how most people already invest in the stock market: a 401(k) takes a fixed contribution from every paycheck, often topped up by an employer match, and buys the same fund on the same schedule whatever the price.
This project replays that 401(k)-style plan against real historical prices for SPY — the SPDR S&P 500 ETF Trust, which has tracked the S&P 500 since January 1993 — so you can see what a given contribution schedule would have produced.
S&P 500 DCA Calculator — a browser-based simulator. Set a start date, how often you contribute, how much, an optional employer match and whether dividends are reinvested, and the portfolio value, return figures, growth chart and full purchase history update live as you go.
Dividends are reinvested by default. The prices are SPY's actual traded closes — adjusted for splits, not for dividends — which is precisely what makes reinvestment something the model can do explicitly rather than assume. Reinvest dividends is on out of the box, because that is what a 401(k) fund does with a distribution. Untick it and every figure is price return only, which understates a long plan by roughly 1–2% a year. The page says which of the two it is showing, everywhere it matters.
Price data is pre-baked static JSON committed to this repo — the browser never calls a market-data API.
| Source | Yahoo Finance SPY via yfinance |
| Coverage | 1993-01-29 (SPY's first trading day) → today (8,400+ daily closes) |
| Prices | Actual closing prices in USD — split-adjusted, not dividend-adjusted |
| Dividends | Cash distributions per share, by ex-dividend date — quarterly, 1993 → today |
| Schedule | Weekdays, 21:00 UTC (after the US market close) |
| Credentials | None — no API keys, no repo secrets |
| Script | scripts/fetch_spy_prices.py |
| Output | data/spy-prices.json |
The scheduled Action (.github/workflows/update-spy-data.yml) runs the script, commits data/spy-prices.json if anything changed, and Netlify auto-deploys that commit. It can also be started by hand from the Actions tab. This means:
yfinance needs no key, so there is nothing to expireyfinance library. If it breaks or stalls, the workflow commits nothing, the site keeps serving the last good file, and the header chip turns amber once the newest close is more than three days old. The chip allows five days, so weekends and a single market holiday don't trip it.netlify.toml is load-bearing twice:
publish = "." serves the repo root, which is what makes the page's absolute /data/spy-prices.json request resolve.must-revalidate header on that path is what stops the CDN serving yesterday's prices after a data deploy.├── index.html # the calculator — markup, inline CSS and JS, no build step
├── netlify.toml # publish root + no-cache header for the data file
├── data/
│ └── spy-prices.json # auto-generated, weekdays
├── scripts/
│ └── fetch_spy_prices.py # yfinance SPY → daily closes
├── tests/
│ ├── engine.test.mjs # simulate(), xirr(), the formatters
│ ├── timezone.test.mjs # same plan, three timezones
│ ├── harness.mjs # pulls the inline <script> into a vm
│ └── fixtures/
│ └── prices.json # committed synthetic price series
└── .github/
└── workflows/
├── update-spy-data.yml # weekday cron
└── test.yml # node --test on push and PR
The page fetches /data/spy-prices.json by absolute path, and browsers block
fetch() on file:// outright — so opening index.html by double-clicking it
will always end in the "price data could not be loaded" error. Serve the directory instead:
python3 -m http.server 8000
Then open http://localhost:8000. Any static server works; it just has to serve
the repo root so /data/… resolves. No build step, no package install, no API keys
— the price file is committed, so there is nothing to fetch first. (The chart itself
loads Chart.js from jsDelivr; without it, the figures and purchase history still render.)
Rebuilding the price data is only needed for fresher numbers than the committed file, and needs no credentials:
pip install yfinance && python3 scripts/fetch_spy_prices.py
That rewrites data/spy-prices.json in place — exactly what the scheduled Action does.
data/spy-prices.json (~275 KB) is a standalone, reusable artifact:
{
"generated": "2026-09-15T05:27:33Z", // UTC build time
"source": "Yahoo Finance / yfinance — SPY daily closes (not dividend-adjusted)",
"count": 8463, // length of `prices` only
"prices": [ { "ts": 728265600, "price": 43.94 } ], // ts = UNIX SECONDS (UTC midnight of the trading date), ascending
"dividends": [ { "ts": 732499200, "amount": 0.213 } ] // ts = UNIX SECONDS (UTC midnight of the EX-DIVIDEND date), ascending
}
price is SPY's closing price in USD, rounded to the cent. Days the market was closed
have no entry; a purchase that lands on one fills at the most recent earlier close.
amount is the cash distribution in USD per share with that ex-dividend date —
SPY pays quarterly, so there are a few hundred entries against several thousand closes.
Because the closes are not dividend-adjusted, the two arrays are independent: prices alone
give price return, and the pair gives total return. dividends is optional — a consumer
that ignores it, or an older copy of the file that predates it, still gets exactly the
price-only result.
No dependencies and no install step — the engine is exercised straight out of
index.html:
node --test tests/*.test.mjs
tests/harness.mjs extracts the inline <script>, evaluates it in a node:vm
context against a minimal DOM stub, and hands back the pure functions. The boot
code at the end of the script is cut off, so nothing fetches or renders.
engine.test.mjs — golden results for several plans, one of them with an
employer match (purchase counts, totals, the contribution/match split, units,
final value, ROI, average cost and the last history row), XIRR against
closed-form cases including one it cannot solve, the averaging effect
invariant that average cost never exceeds the mean price paid, the guard that a
purchase never fills at a close later than its own date, the start-date and
match rules, and the formatters. A second set of goldens covers dividend
reinvestment: the purchased/dividend share split, the price basis, the window
edges (an ex-date before the first purchase, and one past the end date), a plan
whose window contains no distribution at all, and the regression guard that with
reinvestment off — or with no dividend series to run against, as an older
cached entry has — every figure is bit-for-bit the price-only one.heatmap.test.mjs — the start-date heatmap's grid builder: that a cell is
simulate()'s own annual for the same plan (and reports the same "no rate
solves" where simulate() does), that a cell is unchanged by the contribution
amount or the employer match, that reinvestment moves every cell it touches
upward and leaves a window with no ex-date in it alone, the row and column
geometry (a holding period the data cannot fill is dropped whole; no cell's
window runs past the last close), and the per-row worst / median / best /
share-positive figures against hand-worked values, in both dividend modes.timezone.test.mjs — runs the same plans in child processes under TZ=UTC,
TZ=America/New_York and TZ=Australia/Sydney and requires byte-identical
output. The schedule steps with setUTCDate(); stepping in local time would
drift by an hour across a daylight-saving change and can roll a purchase back
onto the previous UTC day, filling it at the previous close.Every expectation runs against tests/fixtures/prices.json — a small, committed,
deterministic series in the same schema as the real file, including a short quarterly
dividends array — and against an end date passed into simulate(). Nothing depends on
data/spy-prices.json or on today's date, so the weekday data job can never turn the
suite red.
The calculator supports:
?start=2000-01-03&freq=14&amt=100&match=50), and Copy link puts it on the clipboard.
Reinvestment is the default, so only switching it off is carried, as &div=0 — ordinary
links stay cleanFor each purchase date, the simulator binary-searches for the most recent SPY close at or before that date — never a later one, so the model can't look ahead — buys (contribution + match) ÷ price in fractional shares, and calculates:
Results update live — there is no Run button. The page leads with the portfolio value and a plain-English summary of the plan, then six KPI tiles, a chart of portfolio value against total invested, an averaging effect card, a collapsible start-date heatmap, and a collapsible purchase history with sortable columns and CSV export.
Did your start date matter? — a collapsed panel under the averaging card, answering the question the rest of the tool keeps implying: how much of an outcome is the month you happened to begin, and how much of that fades as the holding period grows.
Rows are holding periods of 1, 2, 3, 5 and 10 years; a period the price history cannot
fill even once is dropped rather than shown empty. Columns are every calendar month
from the 1993 floor up to the last month whose window still finishes on or before the
newest close — so the grid's right edge is ragged, and the longer the hold, the sooner
it stops. Each cell is a whole plan of its own, run through the same simulate() the
headline figures use, and coloured by its money-weighted annualised return on a
diverging scale clamped symmetrically at ±40%/yr — wide enough for all but about 1% of
SPY's windows. Your own start month is outlined in the accent colour.
A cell does not depend on how much goes in. Scaling every contribution — or adding an employer match on top of it — scales the cash flows by the same factor and leaves the rate that discounts them to zero exactly where it was, so the grid is a function of the price history, the frequency and the dividend switch alone. Changing the amount, the match or the start date repaints it (to move the marker) but never recomputes it; changing the frequency, or Reinvest dividends, does recompute, and abandons any pass still in flight. Each (frequency, reinvestment) pair's finished grid is cached, so going back to one is instant.
Building it is around 1,770 simulations on the SPY series (and one more column every
month), so it runs in frame-sized chunks rather than one blocking pass, seeding each
XIRR solve with the previous cell's answer to cut Newton's iterations. Nothing is
computed until the panel is first opened. Below the canvas, each row's worst, median and
best outcome and the share of start months that ended positive are written out as text —
the canvas itself is a
labelled role="img" that takes focus, so the arrow keys walk it cell by cell and the
colour key is aria-hidden — no figure is ever reachable by hover alone.
These are stated on the page too, under How this works, but they matter to anyone reading the numbers:
data/spy-prices.json is paid on the shares held going into its ex-date and immediately buys more at that day's close. A real fund reinvests on the later pay date, a few weeks after: this is an approximation of one, not a replica of it, and it will differ a little either way depending on what the price did in between. A purchase made on an ex-date earns nothing from that distribution — buying at the close is too late to be on the register.(value / invested)^(1/years) shortcut — which this tool's first version used — credits every dollar with the full elapsed period, and materially understates a contribution stream whose latest dollars have barely had time to compound — on a bi-weekly plan since January 2000 it reports about 5.8%/yr against a true 9.8%/yr (as of September 2026).…-reinvested.csv or …-price-only.csvDisclaimer: This tool is for educational and informational purposes only. Past performance does not guarantee future results, and the figures here exclude fees and taxes — and exclude dividends too, unless Reinvest dividends is ticked. Equity markets can fall as well as rise — always conduct your own research before making any investment decisions.