Oracle3
Oracle3: 예측시장 거래 엔진·MCP 서버
시작일·간격·금액을 입력해 실제 SPY 종가 기준 S&P 500 평균 매수 효과를 계산
Free S&P 500 dollar-cost-averaging calculator. Replay a 401(k)-style contribution plan, with an employer match, on real SPY daily closes to see what it would be worth today.
금융HTML · JavaScriptGitHub @00xJS온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.