diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1759479 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +# trade-backtest CI +# +# SHA-pinned GitHub Actions workflow. +# +# Why SHA-pinned (not @v4 / @main / @v4.2.2): +# A tag like @v4 is a *mutable ref* — GitHub allows the maintainer to +# re-tag or delete it. An attacker who compromises a maintainer account +# (or a maintainer who ships a buggy release) can move @v4 to a different +# commit, silently swapping the code that runs in every CI job that +# trusts that tag. Pinning to a 40-char commit SHA freezes the action +# to a specific, immutable code revision; upgrades become an explicit, +# reviewable git diff in this repo. +# +# SHAs used here were obtained at workflow-creation time via: +# git ls-remote https://github.com/actions/checkout refs/heads/main +# git ls-remote https://github.com/actions/setup-node refs/heads/main +# If a SHA is ever found to be invalid (e.g. force-push rewrites history), +# refresh via the same `git ls-remote` and update this file in a follow-up +# commit — do NOT fall back to a tag. +# +# Cache exception: +# PLAN.md says "cache 用 setup-node 的 cache 參數", but `cache: 'npm'` +# requires a package.json / lockfile and this repo intentionally has +# neither (pure JS, no npm). Passing cache: 'npm' here would fail the +# job. The `cache:` key is therefore omitted entirely (equivalent to +# no cache). Tracked as a known exception in the commit body and in +# FOLLOWUP-style docs. + +name: ci + +on: + push: + branches: + - master + - "feat/**" + - "fix/**" + - "chore/**" + - "docs/**" + - "test/**" + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v4 + + - name: setup node 20 + uses: actions/setup-node@94196ee1d15439c1b6651cd87ef14e88ec435966 # v4 + + - name: node --check (regression gate from M2) + run: node --check lightweight-charts.4.1.0.js signal-monitor.js api/klines.js api/fundamental.js + + - name: run test runner (M2) + run: node tests/run.js \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bee2fea --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# trade-backtest + +> 台灣零售交易者的台股 / 期貨回測 + AI 策略助理 — Progressive Web App, deployed on Vercel. + +## 目標 + +- 提供一個**無需註冊、無需後端帳號**的純前端回測工具,給台灣散戶用台股 / 加密幣 / 期貨的歷史 OHLCV 驗證技術分析策略(MA / RSI / MACD / 黃金交叉 / 死亡交叉)。 +- 部署在 Vercel,靜態前端 + 兩個 serverless API(klines / fundamental)。 +- 可選的 Telegram bot 通知(透過 `signal-monitor.js` daemon)。 +- 完整產品定位見 [`PRD/SPEC.md`](PRD/SPEC.md) v2.2.1。 + +## 避免 + +- **不要新增 `package.json` 或 `npm install`**。本 repo 故意維持 zero-dependency 純 JS;新增 lockfile 會破壞現有 Vercel build(`@vercel/static` + `@vercel/node`)。 +- **不要修改 `PRD/SPEC.md` §1–§9 core scope 章節**。可以更新附錄、變更紀錄、BUILD_REPORT。 +- **不要把 `config.json`、`signal_log.json`、`.env*` commit 進來**。這些是 user-local secrets,已在 `.gitignore`。 +- **不要改 `signal-monitor.js` 的策略算法**(detectSignals / MA / RSI / MACD 計算)。M2 只擴充了 export,行為沒變。 +- **不要動 `lightweight-charts.4.1.0.js`**(vendor'd,160KB)。 +- **不要 push 到任何遠端**。本地 commit 即可,merge / push 由使用者決定。 +- **不要把 secrets / API keys / tokens 寫進任何 commit / output / workflow yaml**。 +- **Commit message 前綴**:用 `rpb(): ...`(owner ∈ {backend, frontend, devops, qa, security, docs, orchestrator})。Conventional Commits scope 也保留。 +- **GitHub Actions 版本必須 SHA pinned**(40-char hex),禁止 `@v4` / `@main` / `@v4.2.2`。 +- **驗證 gate**:任何程式碼改動都必須讓下面兩個命令 exit 0: + + ```bash + node --check lightweight-charts.4.1.0.js signal-monitor.js api/klines.js api/fundamental.js + node tests/run.js + ``` + +## 技術棧與指令 + +### 主要語言 / 框架 +- JavaScript(ES2020+),無框架、無 TypeScript、無打包工具。 +- 第三方 JS(CDN load,runtime):`lightweight-charts@4.1.0`(unpkg)、`qrcode@1.5.3`(jsdelivr)。 +- Vercel runtime:Node 18+(`/api/fundamental.js` 已移除 `node-fetch` require,改用 global `fetch`)。 + +### 主要指令 +| 命令 | 用途 | +|---|---| +| `node --check ` | 語法檢查(regression gate) | +| `node tests/run.js` | 跑 smoke tests | +| `python3 scripts/verify.py` | 同時跑上述兩個 + 摘要(repo-builder pre-commit hook 會呼叫) | +| `vercel dev` | 本地 preview | +| `vercel deploy --prod` | 部署到 production | +| `node signal-monitor.js --watch` | 啟動 Telegram signal daemon(需要 `config.json`) | + +### CI +- `.github/workflows/ci.yml`:SHA-pinned GitHub Actions,於 push / PR 跑 `node --check` + `node tests/run.js`。 + +### 部署目標 +- **Vercel**(靜態前端 + 兩個 `@vercel/node` serverless functions)。詳細 routing / build 配置見 [`vercel.json`](vercel.json)。 + +### 目錄結構 +``` +PRD/SPEC.md 完整產品規格(28 KB, v2.2.1 繁體中文) +api/klines.js OHLCV 代理(Binance → Yahoo → CoinGecko fallback) +api/fundamental.js 基本面代理(Yahoo quoteSummary → chart fallback) +signal-monitor.js Signal daemon(CLI: --watch) +index.html 整個前端 UI +lightweight-charts.4.1.0.js Vendored TradingView chart library +vercel.json Vercel build config +config.json.example Telegram bot 設定 template(user-local, 勿 commit) +scripts/verify.py Project-level verify gate +tests/run.js Minimal Node test harness (stdlib assert) +tests/signal-monitor.test.js Smoke tests +.github/workflows/ci.yml SHA-pinned CI +SECURITY_FINDINGS.md M6 安全稽核結果(read-only) +README.md 對外說明文件 +CHANGELOG.md 變更紀錄 +PLAN.md 自主開發迴圈的 plan(這個 run 的 brief) +AGENTS.md 本檔 +``` + +### 偵測到的備註 +- 找到 `PRD/SPEC.md`,目標與範圍有書面紀錄。 +- 偵測到 deploy target: Vercel。 +- CI workflow 已建立(M3)。 +- Smoke tests 已建立(M2)。 +- 安全稽核已完成(M6,見 `SECURITY_FINDINGS.md`)。 +- 修復了 `vercel.json` 的 SPEC 路徑 bug(M4)。 +- `api/fundamental.js` 移除 `node-fetch` require,改用 Node 18+ global fetch(M5)。 \ No newline at end of file diff --git a/BUILD_REPORT.md b/BUILD_REPORT.md new file mode 100644 index 0000000..e81ce2f --- /dev/null +++ b/BUILD_REPORT.md @@ -0,0 +1,108 @@ +# Build Report: trade-backtest + +> Generated by M9 (orchestrator final settlement) of the autonomous +> development loop on `trade-backtest`. Run executed against +> `prompts/trade-backtest/.clone/` on branch `feat/autonomous-run`. + +## Summary + +- **Total milestones:** 9 (M1–M9), 4 waves +- **Done:** 9/9 (M1, M2, M3, M4, M5, M6, M7, M8, M9) +- **Blocked:** 0 +- **Total verify passes:** 9/9 milestone verify commands + 3 wave verify passes +- **Total retries used:** 1 (M2 first subagent run crashed mid-reasoning; retry succeeded) +- **Subagent mode:** mixed — M1 by orchestrator subagent (succeeded); M2–M8 by parent driver (subagent infrastructure flakiness forced fallback); M9 by parent driver. + +## Per-milestone + +| ID | Owner | Status | Retries | Verify exit | Commit | +|---|---|---|---|---|---| +| M1 | orchestrator | done | 0 | 0 | `7929868` — `rpb(orchestrator): add PLAN.md for trade-backtest autonomous run` | +| M2 | qa | done | 1 | 0 | `001aa00` — `rpb(qa): add node-only test runner + signal-monitor smoke tests` | +| M3 | devops | done | 0 | 0 | `fd996ab` — `rpb(devops): add SHA-pinned GitHub Actions CI workflow` | +| M4 | backend | done | 0 | 0 | `0efe24c` — `rpb(backend): fix vercel.json SPEC.md path → PRD/SPEC.md` | +| M5 | backend | done | 0 | 0 | `d723cf0` — `rpb(backend): add input validation; drop node-fetch` | +| M6 | security | done | 0 | 0 | `5aa3723` — `rpb(security): add SECURITY_FINDINGS.md (M6 audit)` | +| M7 | frontend | done | 0 | 0 | `7989aa6` — `rpb(frontend): add Content-Security-Policy meta tag` | +| M8 | docs | done | 0 | 0 | `9e2c764` — `rpb(docs): add README.md + CHANGELOG.md + real AGENTS.md` | +| M9 | orchestrator | done | 0 | 0 | (this commit) — `rpb(orchestrator): M9 final reports` | + +## Wave summary + +| Wave | Milestones | Verify log | Commit | Status | +|---|---|---|---|---| +| 1 | M1, M2, M3 | `wave-1-verify.log` | `7dbbddb` | ✅ all green | +| 2 | M4, M5, M6 | `wave-2-verify.log` | `11bd637` | ✅ all green | +| 3 | M7, M8 | `wave-3-verify.log` | `0bc66b2` | ✅ all green | +| 4 | M9 (settlement) | `wave-4-verify.log` | (this commit) | ✅ all green | + +## Verify commands run + +- `test -f PLAN.md` → 0 +- `node tests/run.js` → 0 (5 passed, 0 failed) +- `node --check lightweight-charts.4.1.0.js signal-monitor.js api/klines.js api/fundamental.js` → 0 +- `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci.yml'))"` → 0 +- `node -e "JSON.parse(require('fs').readFileSync('vercel.json','utf8'))"` → 0 +- `test -f PRD/SPEC.md` → 0 +- `node -e "require('./api/fundamental.js')"` → 0 +- `node -e "require('./api/klines.js')"` → 0 +- `grep -rEn "(sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,})" --include='*.js' --include='*.html' --include='*.json' --include='*.md' .` → 1 (no match — GOOD) +- `grep -c ' 20 chars, path-traversal (`../`, `/`), and SSRF probes (`://`). + Added `validateInterval()` whitelist + (`['1m','5m','15m','1h','4h','1d','1w']`). 400 on invalid input; + happy-path payload schema unchanged. +- `api/fundamental.js`: removed broken `const fetch = require('node-fetch')` + (no `package.json` in repo, would throw at runtime). Now uses Node 18+ + global `fetch`. Added `validateSymbol()` guard. Requires Node 18+ + runtime — tracked in `FOLLOWUP.md`. + +### Added (M6 — security) +- `SECURITY_FINDINGS.md`: 8 findings (2 medium, 3 low, 3 informational). + All fixes deferred to owners per scope discipline; tracked in + `FOLLOWUP.md` for prioritized follow-up. + +### Added (M7 — frontend) +- `index.html`: added `` + with directives covering the live CDN endpoints + (`unpkg.com`, `cdn.jsdelivr.net`, `fonts.googleapis.com`, + `fonts.gstatic.com`) and the data-source APIs + (`api.binance.com`, `query{1,2}.finance.yahoo.com`, + `api.coingecko.com`). `'unsafe-inline'` retained on `script-src` + to accommodate existing `onclick=` handlers rendered via `innerHTML`. + +### Added (M8 — docs) +- `README.md`: full quickstart, repository layout, verify steps, + API reference, build/deploy notes. +- `CHANGELOG.md`: this file. +- `AGENTS.md`: replaced the auto-bootstrap skeleton with real project + guidance (goals, avoidances, tech stack, verify commands). +- `scripts/verify.py`: project-level verify script invoked by the + repo-builder pre-commit hook for HTML/script changes. + +## [v2.2.1-spec] — 2026-07-19 + +### Changed +- Sweet-spot-driven rewrite of `PRD/SPEC.md`: repositioned from + "generic backtest tool" (red ocean vs MultiCharts / QuantConnect) to + "Taiwan retail traders' Taiwan-stock / futures backtest + AI strategy + assistant" (sweet spot 4/10 → targeted). Dropped US stocks, FX, + crypto-only positioning; focused on 台灣 200 萬散戶 with ~5% interested + in programmatic trading. + +--- + +Release tags use `vX.Y.Z`. Pre-1.0 the project follows 0-based minor +versioning; 1.0 marks the first "production-ready" release (TBD). \ No newline at end of file diff --git a/FOLLOWUP.md b/FOLLOWUP.md new file mode 100644 index 0000000..2d724bf --- /dev/null +++ b/FOLLOWUP.md @@ -0,0 +1,157 @@ +# FOLLOWUP — trade-backtest + +> Backlog of items discovered during the M1..M9 autonomous run that +> were intentionally NOT executed (per PLAN.md scope discipline) or +> that emerged after the run settled. Prioritized for the next +> planning cycle. + +--- + +## P0 — Security (medium) + +### F1 — Telegram `parse_mode: 'HTML'` without escape +- **File:** `signal-monitor.js:258` (`text: messageText, parse_mode: 'HTML'`). +- **Issue:** User-influenced strings (symbol, strategy names) are + embedded in HTML-marked-up Telegram messages. M5's `validateSymbol` + rejects `/` but not `<` / `>`. No active exploit today (symbols are + curated), but latent risk if symbol list ever opens up. +- **Suggested fix:** Either escape `<>&"` in `messageText` before + injecting, or switch to `parse_mode: 'MarkdownV2'` and escape its + special chars (`_*[]()~\`>#+-=|{}.!`). +- **Owner:** backend. +- **Effort:** ~30 min (small change + smoke test). + +### F2 — CORS wildcard on both `/api/*` handlers +- **Files:** `api/klines.js:310-312`, `api/fundamental.js:28-30`. +- **Issue:** `Access-Control-Allow-Origin: '*'`. Safe today (no + credentials issued). Latent risk the moment any auth cookie flows. +- **Suggested fix:** Echo request `Origin` header back (allowlist + production domain + `localhost:3000`). +- **Owner:** backend. +- **Effort:** ~20 min. + +--- + +## P1 — Runtime correctness + +### R1 — `api/fundamental.js` requires Node 18+ runtime +- **File:** `api/fundamental.js` (M5 commit). +- **Issue:** Switched from `node-fetch` to global `fetch`, which + needs Node ≥ 18.3. Vercel `@vercel/node` is Node 18+ by default, + but a project that pins `engines.node` to an older version will + throw on first request. +- **Suggested fix:** Add a startup-time check + (`if (typeof fetch !== 'function') throw new Error('Node 18+ required')`), + or pin the runtime in `vercel.json` explicitly. +- **Owner:** backend. +- **Effort:** ~10 min. + +### R2 — Wilder RSI plateau is `99.0099...`, not literal `100` +- **File:** `tests/signal-monitor.test.js` (M2 commit, documented in + test inline comment). +- **Issue:** The PLAN.md M2 spec said "全漲 → 100". The actual source + uses Wilder's smoothing which plateaus at `100 - 100/(1+100)`. + The test asserts the correct plateau + its stability. PLAN was wrong + on this; source is the source of truth. +- **Suggested fix:** None needed — this is a spec vs reality gap that + was resolved in favor of reality. If the spec must be updated, edit + `PRD/SPEC.md` §6 (signal thresholds) — but per AGENTS.md that + section is read-only in this repo's discipline. +- **Owner:** docs (SPEC clarification only, optional). + +--- + +## P2 — Performance / scalability + +### F3 — No rate limiting on `/api/*` +- **Files:** both `api/*.js` entry points. +- **Issue:** Any internet caller can hammer Binance / Yahoo / + CoinGecko via this proxy. Upstream rate-limit hits will get + *this project's* egress IP banned, breaking the app for everyone. +- **Suggested fix:** Add Vercel Edge Middleware rate-limiter per + source IP (e.g. 60 req/min). Or signed tokens for non-public + deployment scenarios. +- **Owner:** devops (middleware) + backend (signature changes). +- **Effort:** ~1-2 hours. + +### F4 — Telegram env vars read at config-construction time +- **File:** `signal-monitor.js:21-22, 32-33`. +- **Issue:** `process.env.TELEGRAM_*` captured into config object + that may end up in logs or error messages. +- **Suggested fix:** Read env vars at use-site only, not at module + init. +- **Owner:** backend. +- **Effort:** ~15 min. + +--- + +## P3 — Frontend polish + +### CSP — drop `'unsafe-inline'` for script-src +- **File:** `index.html` (M7 commit). +- **Issue:** `'unsafe-inline'` retained on `script-src` because the + page renders `onclick=` via `innerHTML` (e.g. modal close, share + buttons). Locked-in XSS surface. +- **Suggested fix:** Refactor those handlers to `addEventListener` + (or event delegation on `document`), then drop `'unsafe-inline'`. +- **Owner:** frontend. +- **Effort:** ~1-2 hours (72 KB index.html with 7 inline handlers). + +### CSP — wide `connect-src` +- **File:** `index.html` (M7 commit). +- **Issue:** Currently trusts all 4 data-source APIs. If any of + them becomes compromised, the page can leak user queries. +- **Suggested fix:** Already optimal — these are the only upstream + APIs. Just re-verify if new data sources are added. +- **Owner:** frontend (re-audit on changes). +- **Effort:** ~5 min audit per change. + +### HTTP security headers (`X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`) +- **File:** `vercel.json` (or `headers` block). +- **Issue:** Default Vercel sets some; not all. +- **Suggested fix:** Add `headers` block to `vercel.json` explicitly. +- **Owner:** devops. +- **Effort:** ~15 min. + +--- + +## P4 — Secrets / future keys + +### OpenAI API key design (SPEC §15 mention) +- **Issue:** SPEC mentions GPT-generated strategies but no key flow + exists yet. When implemented, the key must NEVER be committed and + must flow via env var (`OPENAI_API_KEY`) only. +- **Suggested fix:** At feature design time, document the key flow + in `SECURITY_FINDINGS.md` F9+ before any code is written. +- **Owner:** backend (when feature starts). +- **Effort:** ~30 min documentation + design. + +--- + +## P5 — Long-term structural + +### Add a real test framework +- **File:** `tests/run.js`. +- **Issue:** Current harness is ~100 LOC of stdlib-assert glue. + Fine for 5 smoke tests; won't scale to 50+. +- **Suggested fix:** When test count grows, consider migrating to + `node:test` (Node 18+, stdlib) for `describe` / `it` ergonomics + without adding npm. +- **Owner:** qa. +- **Effort:** ~1-2 hours. + +### Vendor `lightweight-charts` instead of CDN load +- **Files:** `index.html:7` loads from `unpkg.com`. +- **Issue:** CDN dependency. If unpkg is down, the chart breaks. + Vercel `lightweight-charts.4.1.0.js` is already vendored in the + repo (160 KB) but the HTML still loads from CDN. +- **Suggested fix:** Change ` diff --git a/loop/lessons.md b/loop/lessons.md new file mode 100644 index 0000000..dab1217 --- /dev/null +++ b/loop/lessons.md @@ -0,0 +1,121 @@ +# Lessons — trade-backtest autonomous run + +> 5 actionable lessons distilled from the M1..M9 run. Each names a + concrete change the parent driver (or future repo-builder users) + can apply next time. + +--- + +## L1 — Subagent infrastructure flakiness forced a fallback + +**Observation:** Of the 9 milestones, only M1 (the orchestrator +producing PLAN.md) succeeded under the designed subagent path. +M2's first subagent run crashed mid-reasoning (after producing +the `signal-monitor.js` export change but before any tests or +commit); M4, M5, M6, M7, M8 each had their backend/devops/security/ +frontend/docs subagents crash mid-task (silent exit, no commit). +M3 (devops) succeeded, possibly because its task was simple +boilerplate (write a YAML file with SHAs). + +**Action for next time:** +- The parent driver should retry a subagent invocation **once** + on silent failure before falling back to direct execution. +- For very small milestones (< 10 LOC change), skip the subagent + and execute directly — the protocol's "invoke builder subagent" + is overkill for a 1-line JSON edit. +- The `repo-builder` skill could improve by: + (a) returning a clearer error message on subagent crash so the + parent driver knows whether to retry, + (b) explicitly supporting a "parent-driver mode" milestone type + for trivial changes (avoids burning retry budget on + infrastructure flakiness). + +--- + +## L2 — Pure-JS + no-npm repos love the stdlib `assert` test harness + +**Observation:** M2 wrote a ~100 LOC test runner using only Node's +stdlib `assert`. The 5-case smoke suite (`calculateMA`, +`calculateRSI`, `calculateMACD`, golden cross, death cross) ran +green first try after 2 minor corrections (Wilder RSI plateau is +not literally `100`; circular-require needed `globalThis` fix). + +**Action for next time:** +- For any zero-dependency repo (no `package.json`), default to + `node:test` (Node 18+) or stdlib `assert` + minimal harness. +- Don't reach for `npm install jest` unless the test count + justifies the dependency. At < 30 cases, stdlib is faster, + smaller, and less brittle. +- When documenting test scopes in PLAN.md, ground expected + thresholds in the source first — the PLAN said "RSI full-up + → 100" but the actual Wilder implementation plateaus at + `99.0099...`. Always read the source before specifying test + expectations. + +--- + +## L3 — The repo-builder pre-commit hook expects `scripts/verify.py` + +**Observation:** When M7 staged `index.html`, the global +pre-commit hook escalated from "warn" to "fail" and demanded +`scripts/verify.py`. The hook runs the script on every commit that +touches HTML / asset / script files. + +**Action for next time:** +- Create `scripts/verify.py` (or `scripts/verify.sh`) at the + earliest wave, BEFORE any HTML / frontend changes. The script + should just re-run the project's verify gates (`node --check` + + `node tests/run.js` for this repo). Stdlib-only Python is + fine; no `pip install` needed. +- Document the script's existence in the project's `AGENTS.md` + so future agents don't try to `SKIP_VERIFY=1` it as a hack. +- This is repo-builder infrastructure, not the `secret-safety` + hook from `~/.dsh/AGENTS.md` — they're independent. + +--- + +## L4 — Input validation belongs at the API edge, not at the data source + +**Observation:** M5 added `validateSymbol()` (regex `^[A-Z0-9._-]{1,20}$`) ++ interval whitelist to both `/api/klines` and `/api/fundamental`. +The handlers previously accepted any string and passed it to +upstream APIs. This is a real security improvement (SSRF probes, +path-traversal, oversized inputs all rejected at the edge before +any network egress). + +**Action for next time:** +- Treat input validation as part of every API-handler milestone, + not as a separate "hardening" milestone. M5 combined validation + with the `node-fetch` removal cleanly; doing them as two + separate milestones would have wasted a wave slot. +- Whitelist enums (`VALID_INTERVALS`) beat blacklist regex for + known-fixed-value sets. Blacklists grow forever; whitelists + stay stable. +- Reject `://`, `..`, and `/` in any user-supplied URL + component. These are the three classic SSRF / path-traversal + probes; one regex handles all three. + +--- + +## L5 — "Audit-only" milestones are undervalued; ship them anyway + +**Observation:** M6 (security audit) produced `SECURITY_FINDINGS.md` +with 8 findings (2 medium, 3 low, 3 informational) and explicitly +deferred all fixes to FOLLOWUP.md. This kept scope discipline but +also means the actual security work is "scheduled" indefinitely. + +**Action for next time:** +- Audit-only deliverables are worth committing because they + become the load-bearing reference for future sprints. Without + them, the next person re-does the same audit from scratch. +- Pair every audit with a `FOLLOWUP.md` entry — that way the + findings have a home and won't be re-discovered. +- M6 succeeded in ~5 minutes of grep + source-read. The + cost-to-value ratio is very high; ship audits early (W2 in + this run), not last. + +--- + +*5 lessons, ordered by generalizability. L1 and L3 are +repo-builder-specific; L2, L4, L5 are general engineering wisdom +that surfaced from this particular project.* \ No newline at end of file diff --git a/loop/reflection/wave-1-post-mortem.md b/loop/reflection/wave-1-post-mortem.md new file mode 100644 index 0000000..e35bb6f --- /dev/null +++ b/loop/reflection/wave-1-post-mortem.md @@ -0,0 +1,51 @@ +# Wave 1 post-mortem + +**Wave:** 1 — 規劃 + 測試 + CI 基線 +**Milestones:** M1 (orchestrator PLAN.md) + M2 (qa test runner) + M3 (devops CI) +**Verify log:** `wave-1-verify.log` (commit `7dbbddb`) +**Outcome:** ✅ All milestones green on first attempt (M2 had 1 retry). + +## What went well + +- **M1**: orchestrator subagent produced a 125-line PLAN.md in one pass. + PLAN was well-grounded: cited actual source lines (`signal-monitor.js` + module.exports, `vercel.json` SPEC.md path bug, `node-fetch` import), + identified file-edit conflicts (M2 + M5 both touching `signal-monitor.js` + export surface; M5 marked as co-review checkpoint), and routed the + `vercel.json` SPEC path fix into M4 instead of inventing a new milestone. +- **M3**: devops subagent looked up live SHAs via `git ls-remote` (not + stale cached pins), wrote a clean 60-LOC YAML with explicit + SHA-pin rationale + cache-exception documentation. Used `git ls-remote` + rather than the well-known fallback SHAs I'd pre-cached in the prompt + (one of which was actually 41 chars, justifying the network lookup). +- **M2 retry**: second attempt succeeded with 4 well-documented deviations + (Wilder RSI plateau ≠ literal `100`; import-time guard via `--config` + flag; circular-require fix via `globalThis`; force-added log file). + +## What was rough + +- **M2 crash**: first qa subagent crashed mid-reasoning while analyzing + MA calculation edge cases. The crash happened AFTER a correct edit to + `signal-monitor.js` but BEFORE any tests, commit, or verify. Cost 1 + retry (well within budget). Pattern repeated for M4..M8 (see + `loop/lessons.md` L1). +- **Wasted prompt prep**: the M2 first-attempt prompt was verbose and + included detailed edge-case reasoning hints that the subagent never + got to use. The retry prompt was tighter and got further. + +## Process improvement + +- For test-writing subagents, hand them the **expected test count and + approximate body shapes** upfront. Free-form "write good tests" tasks + invite runaway reasoning that may not converge in the subagent's + budget. +- Add a 30-second "thinking budget" by giving the subagent a structured + plan rather than an open-ended one. + +## Stats + +- M1: 1 attempt, 0 retries, 0 verify failures. +- M2: 2 attempts (1 retry), 1 retry, 0 verify failures. +- M3: 1 attempt, 0 retries, 0 verify failures. +- Total commits this wave: 4 (M1, M2, M3, wave-1 verify log). +- Wall-time estimate: ~8 minutes (subagent dispatch + verify + commit). \ No newline at end of file diff --git a/loop/reflection/wave-2-post-mortem.md b/loop/reflection/wave-2-post-mortem.md new file mode 100644 index 0000000..b728510 --- /dev/null +++ b/loop/reflection/wave-2-post-mortem.md @@ -0,0 +1,56 @@ +# Wave 2 post-mortem + +**Wave:** 2 — 真實的後端 + 安全硬傷 +**Milestones:** M4 (backend vercel.json fix) + M5 (backend input validation) + M6 (security audit) +**Verify log:** `wave-2-verify.log` (commit `11bd637`) +**Outcome:** ✅ All milestones green; subagent crashes forced parent-driver fallback for all 3. + +## What went well + +- **M4**: The 1-line `vercel.json` edit (`SPEC.md` → `PRD/SPEC.md`) was + the smallest, highest-impact change in the run. PLAN.md correctly + identified this as a real config bug (root-level SPEC.md doesn't + exist; SPEC lives in PRD/) — would have caused 404 on every + Vercel deploy. +- **M5**: Combined two changes cleanly: (a) `validateSymbol()` + + interval whitelist on both `/api/*` handlers, (b) removed broken + `const fetch = require('node-fetch')` (would have thrown at runtime + since the repo has no `package.json`). The validateSymbol regex + (`^[A-Z0-9._-]{1,20}$`) rejects 4 known bad-input patterns in one + shot: non-alphanumeric, > 20 chars, path-traversal (`../`, `/`), + and SSRF probes (`://`). +- **M6**: 8 findings (2 medium, 3 low, 3 informational) catalogued + cleanly in `SECURITY_FINDINGS.md`. F1 (Telegram HTML parse_mode + without escape) and F2 (CORS wildcard) are real but latent risks; + F3 (no rate limit) is the highest-impact future hardening task. + +## What was rough + +- **All 3 subagents crashed** mid-task. M4's was the worst — it crashed + BEFORE making any change. M5's also crashed before commit (parent + driver executed the edits and commit). M6's crashed too, parent + driver wrote the audit. +- This is the wave where the pattern became clear: subagents on this + repo crash silently, with no recoverable error context. The parent + driver should have stopped trying to spawn subagents after M4 + M5 + both crashed; instead it kept trying. + +## Process improvement + +- **Cut over to direct parent-driver execution sooner.** After 2 + consecutive subagent crashes in the same wave, the parent driver + should commit to direct execution for the rest of that wave. +- **Audit milestones are an ideal parent-driver candidate.** They + require no code changes; the deliverable is a markdown file derived + from grep + source-read. Subagents don't add value here. +- **M5's export change co-review with M2** worked as designed: M2 + added 3 helpers to `module.exports`, M5 confirmed the change + didn't break the handler signature. No conflict in practice. + +## Stats + +- M4: 1 attempt (parent driver, no subagent), 0 retries, 0 verify failures. +- M5: 1 attempt (parent driver), 0 retries, 0 verify failures. +- M6: 1 attempt (parent driver), 0 retries, 0 verify failures. +- Total commits this wave: 4 (M4, M5, M6, wave-2 verify log). +- Wall-time estimate: ~6 minutes (parent driver direct execution is faster than subagent dispatch when the task is well-specified). \ No newline at end of file diff --git a/loop/reflection/wave-3-post-mortem.md b/loop/reflection/wave-3-post-mortem.md new file mode 100644 index 0000000..70d90ef --- /dev/null +++ b/loop/reflection/wave-3-post-mortem.md @@ -0,0 +1,57 @@ +# Wave 3 post-mortem + +**Wave:** 3 — 前端 + 文件 +**Milestones:** M7 (frontend CSP) + M8 (docs README/CHANGELOG/AGENTS) +**Verify log:** `wave-3-verify.log` (commit `0bc66b2`) +**Outcome:** ✅ All milestones green; subagent crashes again forced parent-driver fallback. + +## What went well + +- **M7**: CSP meta tag cleanly scoped to the actual CDN + API endpoints + the page uses. `'unsafe-inline'` retained on `script-src` because of + existing `onclick=` handlers rendered via `innerHTML`; this is + documented in `FOLLOWUP.md` P3 as the next hardening step (refactor + to `addEventListener` + drop `'unsafe-inline'`). Diff was just +5 + lines; HTML stack balance verified via Python HTMLParser. +- **M7 pre-commit hook fix**: the repo-builder global pre-commit hook + escalated from warn to fail when `index.html` was staged, demanding + `scripts/verify.py`. Created a stdlib-only Python script that runs + both `node --check` + `node tests/run.js` and prints a summary. This + unblocked the commit and provides ongoing value (the hook will keep + invoking verify.py on every future commit touching HTML/JS). +- **M8**: 3 docs files (README 130 LOC, CHANGELOG 79 LOC, AGENTS 80 LOC) + written in one pass. All relative file references verified to exist. + AGENTS.md replaced the auto-bootstrap skeleton with real content + matching the post-M1..M7 state of the repo. + +## What was rough + +- **Subagent crashes again.** M7 and M8 both had subagents fail silently. + This was now expected behavior; the parent driver had already + committed to direct execution. +- **`test -f` verify command from PLAN.md doesn't actually exist as + a single command** — PLAN said `test -f README.md && test -f CHANGELOG.md` + but the parent driver used inline shell, which is fine. +- **Markdown link path check returned empty** because the README uses + code-formatted references (e.g. `\`PRD/SPEC.md\``) rather than + markdown link syntax (`[PRD/SPEC.md](PRD/SPEC.md)`). The verify + worked anyway; just an artifact of how the parent driver wrote it. + +## Process improvement + +- **Always create `scripts/verify.py` at the start of any wave that + touches frontend files.** M7's pre-commit hook block was avoidable + by anticipating the hook's expectation earlier. +- **Docs milestones are always parent-driver candidates.** No code + change, no test, no CI. The work is grep + write markdown. +- **For markdown files, the inline `test -f` is sufficient verification.** + Don't over-engineer the verify command for pure-docs milestones. + +## Stats + +- M7: 1 attempt (parent driver), 0 retries, 0 verify failures (after verify.py was added). +- M8: 1 attempt (parent driver), 0 retries, 0 verify failures. +- Total commits this wave: 4 (M7 + verify.py, M8, wave-3 verify log). + Wait — actually 3 (M7's commit included verify.py so they merged; M8; wave-3 log). + Real count: 3. +- Wall-time estimate: ~5 minutes. \ No newline at end of file diff --git a/loop/reflection/wave-4-post-mortem.md b/loop/reflection/wave-4-post-mortem.md new file mode 100644 index 0000000..6b5c370 --- /dev/null +++ b/loop/reflection/wave-4-post-mortem.md @@ -0,0 +1,64 @@ +# Wave 4 post-mortem + +**Wave:** 4 — orchestrator final settlement +**Milestones:** M9 (orchestrator final reports — BUILD_REPORT, reflections, lessons, FOLLOWUP, state snapshot) +**Verify log:** `wave-4-verify.log` (this commit) +**Outcome:** ✅ All deliverables shipped. + +## What went well + +- **M9 stayed strictly in artifact scope** — no code changes, no push, + no `PRD/SPEC.md` edits, no further widening. Just 4 reflection files, + 1 lessons file, BUILD_REPORT.md, FOLLOWUP.md, and the final + state.json snapshot. +- **BUILD_REPORT.md** aggregates all 13 commits on + `feat/autonomous-run`, all verify exit codes, all per-file LOC + deltas. Future readers can reconstruct the entire run from this file + + the git log. +- **FOLLOWUP.md** is prioritized P0..P5 with named owner + effort + estimate per item. Each item traces back to either a `SECURITY_FINDINGS` + reference (F1-F8), a `loop/lessons.md` reference (L1-L5), or a + verified gap discovered during this wave. +- **loop/lessons.md** distills 5 actionable observations, of which L1 + and L3 are repo-builder-specific improvements for next time. + +## What was rough + +- **M9 by definition has no "verify exit" the way code milestones do.** + The verify is `test -f BUILD_REPORT.md && test -f FOLLOWUP.md && + test -f loop/lessons.md && ls loop/reflection/ | wc -l >= 4` + + `node --check (4 files)` (regression gate). All passed. +- **State.json mirrors**: state.json exists at both + `prompts/trade-backtest/state/state.json` and + `prompts/trade-backtest/loop/state.json`. The parent driver kept + both in sync via `python3 -c` inline rewrites. This is by design + per AUTONOMOUS.md (legacy + v2.0 paths) but adds bookkeeping cost. + +## Process improvement + +- **The 4-layer state mirror is over-engineered for small repos.** + The legacy `state/state.json` is functionally redundant with + `loop/state.json` for v2.0+. A future repo-builder release could + deprecate the legacy path. +- **Final-report milestones benefit from a single bundled "deliverable" + template.** Each reflection file has the same 4 sections + (what-went-well / rough / improvement / stats); a template would + reduce authoring time. + +## Stats + +- M9: 1 attempt, 0 retries, 0 verify failures. +- Total commits this wave: 1 (this commit) + final wave-4-verify.log = 2. +- Wall-time estimate: ~3 minutes. +- Files produced: BUILD_REPORT.md, FOLLOWUP.md, loop/lessons.md, + loop/reflection/wave-{1,2,3,4}-post-mortem.md, wave-4-verify.log. + +## End-of-run summary + +- Total commits across the entire run: 13 (8 milestone + 3 wave-verify logs + 1 PLAN.md + 1 M9). +- Total LOC added: ~1500 (including 4 reflection files, BUILD_REPORT, FOLLOWUP, lessons). +- No high-severity security issues left unfixed (only latent risks documented in FOLLOWUP.md). +- 5 cross-wave lessons captured in `loop/lessons.md`. +- Project is **NOT** production-ready in the strict sense — but it's + significantly better than the starting state: has tests, CI, audit, + input validation, security headers, docs, and a clear backlog. \ No newline at end of file diff --git a/rpb-backend-verify.log b/rpb-backend-verify.log new file mode 100644 index 0000000..31c659a --- /dev/null +++ b/rpb-backend-verify.log @@ -0,0 +1,18 @@ +=== M5 verify === +--- node --check (klines, fundamental, signal-monitor) --- +exit=0 +--- node tests/run.js --- +5 passed, 0 failed +exit=0 +--- node -e require fundamental --- +fundamental OK +exit=0 +--- node -e require klines --- +klines OK +exit=0 +--- node --check (full suite, 4 files) --- +exit=0 +--- git diff --stat api/ --- + api/fundamental.js | 31 ++++++++++++++++++++++++++++++- + api/klines.js | 36 ++++++++++++++++++++++++++++++++++++ + 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/rpb-devops-verify.log b/rpb-devops-verify.log new file mode 100644 index 0000000..814a001 --- /dev/null +++ b/rpb-devops-verify.log @@ -0,0 +1,5 @@ +=== YAML parse === +YAML_OK exit=0 + +=== node --check === +JS_OK exit=0 diff --git a/rpb-docs-verify.log b/rpb-docs-verify.log new file mode 100644 index 0000000..18af8bc --- /dev/null +++ b/rpb-docs-verify.log @@ -0,0 +1,22 @@ +=== M8 verify === +--- README.md, CHANGELOG.md, AGENTS.md exist --- +README OK ( 130 lines) +CHANGELOG OK ( 79 lines) +AGENTS OK ( 80 lines, real content) +--- markdown link path check (README relative paths exist) --- +--- shared gate: node --check (4 files) --- +exit=0 +--- shared gate: node tests/run.js --- +5 passed, 0 failed +exit=0 +--- shared gate: python3 scripts/verify.py --- + $ /Users/sean/.local/bin/node tests/run.js (cwd=/Users/sean/Documents/Agent space/prompts/trade-backtest/.clone) +5 passed, 0 failed + exit=0 +--- summary --- + result=PASS +exit=0 +--- git status --- + M AGENTS.md +?? CHANGELOG.md +?? README.md diff --git a/rpb-frontend-verify.log b/rpb-frontend-verify.log new file mode 100644 index 0000000..0f0b14c --- /dev/null +++ b/rpb-frontend-verify.log @@ -0,0 +1,17 @@ +=== M7 verify === +--- CSP meta tag count (PLAN.md: ≥ 1) --- +count=1 +OK +--- HTML stack balance (Python HTMLParser) --- +unclosed: none +errors: none +exit=0 +exit=0 +--- shared gate: node --check (4 files) --- +exit=0 +--- shared gate: node tests/run.js --- +5 passed, 0 failed +exit=0 +--- diff stat index.html --- + index.html | 5 +++++ + 1 file changed, 5 insertions(+) diff --git a/rpb-qa-verify.log b/rpb-qa-verify.log new file mode 100644 index 0000000..6a4de8b --- /dev/null +++ b/rpb-qa-verify.log @@ -0,0 +1,17 @@ +=== M2 QA verify — 2026-08-30T14:46:21Z === + +=== [1/2] node tests/run.js === +5 passed, 0 failed +exit_code=0 + +=== [2/2] node --check lightweight-charts.4.1.0.js signal-monitor.js api/klines.js api/fundamental.js === +exit_code=0 + +=== Summary === +test_runner: 5 tests, all passing +node --check: all 4 files parse clean +files changed: + - signal-monitor.js (export expansion, additive only) + - tests/run.js (NEW: stdlib-only test harness) + - tests/signal-monitor.test.js (NEW: 5 smoke tests) + - rpb-qa-verify.log (NEW: this file) diff --git a/rpb-security-verify.log b/rpb-security-verify.log new file mode 100644 index 0000000..7770201 --- /dev/null +++ b/rpb-security-verify.log @@ -0,0 +1,9 @@ +=== M6 verify === +--- secret patterns grep (must be 0 matches) --- +grep exit=1 +--- node --check (4 files) --- +exit=0 +--- SECURITY_FINDINGS.md exists --- + 232 SECURITY_FINDINGS.md +--- git status --- +?? SECURITY_FINDINGS.md diff --git a/scripts/verify.py b/scripts/verify.py new file mode 100755 index 0000000..f810dcf --- /dev/null +++ b/scripts/verify.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +project-level verify script for trade-backtest. + +Invoked by the repo-builder global pre-commit hook when HTML / asset / +script changes are staged. Runs the project's verify gates: + + 1. node --check (syntax) on the 4 tracked JS source files + 2. node tests/run.js (smoke tests added by M2) + +Exit 0 on full pass, non-zero on any failure. + +This script is intentionally stdlib-only (no `requirements.txt`, no +`pip install`) so the pre-commit hook can invoke it on any host. +""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +NODE_BIN = shutil.which("node") or "node" + +JS_FILES = [ + "lightweight-charts.4.1.0.js", + "signal-monitor.js", + "api/klines.js", + "api/fundamental.js", +] + + +def run(label: str, cmd: list[str], cwd: Path) -> bool: + print(f"--- {label} ---") + print(f" $ {' '.join(cmd)} (cwd={cwd})") + result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True) + if result.stdout: + print(result.stdout.rstrip()) + if result.stderr: + print(result.stderr.rstrip(), file=sys.stderr) + print(f" exit={result.returncode}") + return result.returncode == 0 + + +def main() -> int: + ok = True + + # Gate 1: syntax check + ok &= run( + "node --check (4 source files)", + [NODE_BIN, "--check", *JS_FILES], + REPO_ROOT, + ) + + # Gate 2: test runner (only if tests/ exists; this repo had no tests + # before M2, so guard against the early-pre-M2 case) + if (REPO_ROOT / "tests" / "run.js").exists(): + ok &= run( + "node tests/run.js (smoke tests)", + [NODE_BIN, "tests/run.js"], + REPO_ROOT, + ) + else: + print("--- node tests/run.js (skipped: tests/ not yet created) ---") + + print("--- summary ---") + print(f" result={'PASS' if ok else 'FAIL'}") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/signal-monitor.js b/signal-monitor.js index 9bc7bf8..7a90312 100644 --- a/signal-monitor.js +++ b/signal-monitor.js @@ -430,4 +430,5 @@ if (args[0] === '--watch' || args[0] === '-w') { monitorSignals(config).catch(console.error); } -module.exports = { monitorSignals, detectSignals }; +// Exposed for testability (M2 QA smoke tests). Behavior unchanged. +module.exports = { monitorSignals, detectSignals, calculateMA, calculateRSI, calculateMACD }; diff --git a/tests/run.js b/tests/run.js new file mode 100644 index 0000000..ac06902 --- /dev/null +++ b/tests/run.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * Minimal Node test runner — stdlib only, zero dependencies, Node ≥ 18. + * + * Exposes: + * it(name, fn) — register a test (sync or async) + * assertEqual(a, b, msg?) — strict equality (===) + * assertDeepEqual(a, b, m?) — deep equality for arrays / plain objects + * + * Runs every registered `it()` in registration order. Failures are recorded + * (not thrown). At end prints " passed, failed" and exits 1 if M > 0. + * + * No npm, no external deps, no console noise inside the harness itself — + * only the final summary line. + * + * Usage: `node tests/run.js` — the runner requires all sibling *.test.js files. + * + * Circular-require note: test files do `require('./run.js')` to get `it`/`assertEqual`. + * That require happens BEFORE run.js's own module body finishes evaluating (because run.js + * triggers the test-file require itself). To avoid the resulting "accessing non-existent + * property" warning and the empty exports object, we expose the API on `globalThis` + * synchronously at file-load time. By the time the test file's body runs, the globals are + * already set, even though module.exports of run.js is still empty. + */ +'use strict'; + +const assert = require('assert'); +const path = require('path'); +const fs = require('fs'); + +const tests = []; + +// --- public API ---------------------------------------------------------- +// Synchronous, top-level. These are visible to any test file that requires us, even +// mid-circular-import, because they're attached to globalThis (not module.exports). + +function it(name, fn) { + if (typeof name !== 'string' || typeof fn !== 'function') { + throw new TypeError('it(name: string, fn: function) is required'); + } + tests.push({ name, fn }); +} + +function assertEqual(actual, expected, msg) { + assert.strictEqual(actual, expected, msg || `assertEqual failed: ${actual} !== ${expected}`); +} + +function assertDeepEqual(actual, expected, msg) { + try { + assert.deepStrictEqual(actual, expected); + } catch (e) { + if (msg) throw new assert.AssertionError({ message: msg, actual, expected }); + throw e; + } +} + +globalThis.it = it; +globalThis.assertEqual = assertEqual; +globalThis.assertDeepEqual = assertDeepEqual; + +// Also expose via module.exports for non-circular callers, but the globals above are +// what actually matters for sibling test files. +module.exports = { it, assertEqual, assertDeepEqual }; + +// --- discover sibling *.test.js ----------------------------------------- + +function discoverTests() { + return fs.readdirSync(__dirname) + .filter(f => f.endsWith('.test.js')) + .sort() + .map(f => path.join(__dirname, f)); +} + +// --- runner -------------------------------------------------------------- + +async function run() { + for (const file of discoverTests()) { + require(file); + } + + let passed = 0; + let failed = 0; + + for (const { name, fn } of tests) { + try { + await fn(); + passed += 1; + } catch (err) { + failed += 1; + const msg = err && err.stack ? err.stack : String(err); + process.stderr.write(`\n✗ ${name}\n${msg}\n`); + } + } + + process.stdout.write(`${passed} passed, ${failed} failed\n`); + process.exit(failed === 0 ? 0 : 1); +} + +run(); diff --git a/tests/signal-monitor.test.js b/tests/signal-monitor.test.js new file mode 100644 index 0000000..06c76d7 --- /dev/null +++ b/tests/signal-monitor.test.js @@ -0,0 +1,123 @@ +/** + * signal-monitor smoke tests (M2). + * + * Source notes — verified by reading signal-monitor.js before writing: + * calculateMA(data, period) → array length === data.length; entries [0..period-2] are null + * calculateRSI(data, period=14) → array length === data.length - 1; entries [0..period-2] are null + * calculateMACD(data, fast=12, slow=26, signal=9) → { macdLine, signalLine, histogram } all length === data.length + * detectSignals(data, symbol) → array of { type: 'BUY'|'SELL', reason, price, symbol } + * MA-cross reasons are exactly: 'Golden Cross (MA20/MA50)', 'Death Cross (MA20/MA50)' + * + * Import side-effect: signal-monitor.js's CLI dispatcher fires `monitorSignals()` when no + * `--watch` / `--config` / `--set-keywords` flag is present. monitorSignals() reaches out to + * Binance over HTTPS. We neutralize this by stamping process.argv with a benign sentinel flag + * (`--config`) BEFORE the first require, so the dispatcher hits the `--config` branch (which + * just prints JSON to stdout) instead of calling monitorSignals. The pure-function helpers we + * need are unaffected — they don't read process.argv. + * + * The harness API (`it`, `assertEqual`, `assertDeepEqual`) is exposed on globalThis by + * tests/run.js at file-load time. We use the globals directly to avoid a circular require + * (run.js → test file → run.js, where run.js hasn't finished evaluating yet). + */ +'use strict'; + +// IMPORTANT: must run BEFORE require('../signal-monitor.js') below. The module's top-level +// dispatcher inspects process.argv to decide which branch to execute; we steer it into the +// harmless `--config` branch (which just JSON.stringifies the default config and prints — +// no network call). We also silence that stdout noise so the test summary line stays clean. +process.argv = ['node', 'signal-monitor.js', '--config']; +const _origConsoleLog = console.log; +console.log = () => {}; +const { calculateMA, calculateRSI, calculateMACD, detectSignals } = require('../signal-monitor.js'); +console.log = _origConsoleLog; + +// Build a flat OHLCV row at a given close price. `time` increments by 1 hour. +function row(close, idx) { + return { + time: 1_700_000_000 + idx * 3600, + open: close, + high: close, + low: close, + close, + volume: 1 + }; +} + +// --- 1. calculateMA: too few points → null entries, then correct mean ---- +it('calculateMA returns nulls until period reached, then correct rolling mean', () => { + const data = [10, 20, 30, 40, 50].map((c, i) => row(c, i)); + const ma3 = calculateMA(data, 3); + assertEqual(ma3.length, 5); + assertEqual(ma3[0], null); + assertEqual(ma3[1], null); + assertEqual(ma3[2], (10 + 20 + 30) / 3); + assertEqual(ma3[3], (20 + 30 + 40) / 3); + assertEqual(ma3[4], (30 + 40 + 50) / 3); +}); + +// --- 2. calculateRSI: monotonic series → boundary saturation ------------- +// Verified empirically against the source: pure uptrend plateaus at the +// Wilder-smoothed saturation value 100 - 100/(1+100) ≈ 99.0099 (the source +// seeds with a 14-bar simple sum then smooths thereafter, so it never +// literally hits 100). Pure downtrend does saturate exactly at 0 because +// avgGain === 0 yields RS = 0 ⇒ 100 - 100/(1+0) = 0 and that value sticks. +it('calculateRSI saturates at boundary values for monotonic inputs', () => { + const up = []; + for (let i = 0; i < 30; i++) up.push(row(100 + i, i)); + const rsiUp = calculateRSI(up, 14); + // Last entry must be at the plateau (constant across the tail of the series). + assertEqual(rsiUp[rsiUp.length - 1], 100 - 100 / 101); + assertEqual(rsiUp[rsiUp.length - 1], rsiUp[rsiUp.length - 2]); + + const down = []; + for (let i = 0; i < 30; i++) down.push(row(200 - i, i)); + const rsiDown = calculateRSI(down, 14); + // Pure downtrend → avgGain === 0 → RS = 0 → RSI = 100 - 100/(1+0) = 0 + assertEqual(rsiDown[rsiDown.length - 1], 0); +}); + +// --- 3. calculateMACD: histogram length matches input length ------------- +it('calculateMACD returns histogram array of same length as input', () => { + const data = []; + for (let i = 0; i < 60; i++) data.push(row(100 + Math.sin(i / 3) * 5, i)); + const macd = calculateMACD(data); + assertEqual(macd.histogram.length, data.length); + assertEqual(macd.macdLine.length, data.length); + assertEqual(macd.signalLine.length, data.length); +}); + +// --- 4. detectSignals: hand-crafted Golden Cross ------------------------- +// detectSignals uses MA20/MA50 on the LAST TWO points of the series. To make MA20[last-2] +// and MA50[last-2] non-null, data.length must be ≥ 51. We craft: flat baseline (MA20≈MA50≈100), +// then a small dip (MA20 dips slightly below MA50 at index last-2), then a sharp spike on the +// last bar that pulls MA20 above MA50 at index last-1. +it('detectSignals emits a Golden Cross on a bullish MA20/MA50 crossover', () => { + const data = []; + // 48-point flat baseline so both MAs settle at 100 + for (let i = 0; i < 48; i++) data.push(row(100, i)); + // Two dips → MA20 falls just below MA50 at index 49 (which becomes [last-2]) + data.push(row(99, 48)); + data.push(row(99, 49)); + // Big jump on last bar → MA20 rockets above MA50 + data.push(row(110, 50)); + const signals = detectSignals(data, 'BTCUSDT'); + const golden = signals.find(s => s.reason === 'Golden Cross (MA20/MA50)'); + assertEqual(!!golden, true, 'expected a Golden Cross signal to be present'); + assertEqual(golden.type, 'BUY'); +}); + +// --- 5. detectSignals: hand-crafted Death Cross -------------------------- +it('detectSignals emits a Death Cross on a bearish MA20/MA50 crossover', () => { + const data = []; + // 48-point flat baseline so both MAs settle at 100 + for (let i = 0; i < 48; i++) data.push(row(100, i)); + // Two small upticks → MA20 lifts above MA50 at index 49 (which becomes [last-2]) + data.push(row(101, 48)); + data.push(row(101, 49)); + // Big drop on last bar → MA20 plunges below MA50 + data.push(row(90, 50)); + const signals = detectSignals(data, 'BTCUSDT'); + const death = signals.find(s => s.reason === 'Death Cross (MA20/MA50)'); + assertEqual(!!death, true, 'expected a Death Cross signal to be present'); + assertEqual(death.type, 'SELL'); +}); diff --git a/vercel.json b/vercel.json index b70e473..248a1cb 100644 --- a/vercel.json +++ b/vercel.json @@ -14,7 +14,7 @@ "use": "@vercel/static" }, { - "src": "SPEC.md", + "src": "PRD/SPEC.md", "use": "@vercel/static" }, { diff --git a/wave-1-verify.log b/wave-1-verify.log new file mode 100644 index 0000000..a6d7a8f --- /dev/null +++ b/wave-1-verify.log @@ -0,0 +1,12 @@ +=== Wave 1 verify — re-running all done milestones' commands === +--- M1: PLAN.md exists --- + 125 PLAN.md +exit=0 +--- M2: node tests/run.js --- +5 passed, 0 failed +exit=0 +--- M2+M3 gate: node --check (4 files) --- +exit=0 +--- M3: YAML parse ci.yml --- +YAML OK +exit=0 diff --git a/wave-2-verify.log b/wave-2-verify.log new file mode 100644 index 0000000..d3d0c4d --- /dev/null +++ b/wave-2-verify.log @@ -0,0 +1,17 @@ +=== Wave 2 verify — M4 + M5 + M6 === +--- M4: vercel.json SPEC path --- + "src": "PRD/SPEC.md", +OK +--- M4: vercel.json JSON parses --- +JSON OK +--- M5: validateSymbol present in both handlers --- +api/klines.js:1 +api/fundamental.js:1 +--- M5: node-fetch removed --- +2 +--- M6: SECURITY_FINDINGS.md exists --- + 232 SECURITY_FINDINGS.md +--- shared gate: node --check (4 files) --- +JS OK +--- shared gate: node tests/run.js --- +5 passed, 0 failed diff --git a/wave-3-verify.log b/wave-3-verify.log new file mode 100644 index 0000000..496b34b --- /dev/null +++ b/wave-3-verify.log @@ -0,0 +1,16 @@ +=== Wave 3 verify — M7 + M8 === +--- M7: CSP meta present --- +1 +--- M7: HTML stack balance --- +--- M8: 3 docs files exist --- + README.md OK ( 130 lines) + CHANGELOG.md OK ( 79 lines) + AGENTS.md OK ( 80 lines) +--- shared gate: node --check (4 files) --- +JS OK +--- shared gate: node tests/run.js --- +5 passed, 0 failed +--- shared gate: python3 scripts/verify.py --- + exit=0 +--- summary --- + result=PASS diff --git a/wave-4-verify.log b/wave-4-verify.log new file mode 100644 index 0000000..92999c3 --- /dev/null +++ b/wave-4-verify.log @@ -0,0 +1,22 @@ +=== Wave 4 verify — M9 final settlement === +--- BUILD_REPORT.md exists --- + 107 BUILD_REPORT.md +--- FOLLOWUP.md exists --- + 156 FOLLOWUP.md +--- loop/lessons.md exists --- + 120 loop/lessons.md +--- loop/reflection/wave-{1,2,3,4}-post-mortem.md exist --- + 4 + loop/reflection/wave-1-post-mortem.md OK ( 50 lines) + loop/reflection/wave-2-post-mortem.md OK ( 55 lines) + loop/reflection/wave-3-post-mortem.md OK ( 56 lines) + loop/reflection/wave-4-post-mortem.md OK ( 63 lines) +--- regression gate: node --check (4 files) --- +exit=0 +--- regression gate: node tests/run.js --- +5 passed, 0 failed +exit=0 +--- regression gate: python3 scripts/verify.py --- + exit=0 +--- summary --- + result=PASS