尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證An AI agent reads an unstructured news feed and curates a watchlist, while a mechanical mean-variance optimizer handles the sizing that must be auditable. Research project by JMH DataSciences.
翻譯準備中
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
An AI agent reads a continuous feed of unstructured news and makes a routine judgment call a person would otherwise make by hand, while deterministic code handles everything that has to be auditable. The domain here is investing; the pattern is not. Any recurring decision that today requires a person to read the latest unstructured input and re-judge has this shape. The AI names the candidates and rules on whether a thesis still holds; it never sizes a position, because a standard mechanical optimizer does that.檢視完整 README ↗An AI agent reads a continuous feed of unstructured news and makes a routine judgment call a person would otherwise make by hand, while deterministic code handles everything that has to be auditable. The domain here is investing; the pattern is not. Any recurring decision that today requires a person to read the latest unstructured input and re-judge has this shape. The AI names the candidates and rules on whether a thesis still holds; it never sizes a position, because a standard mechanical optimizer does that.
Built end-to-end with Claude Code by Joseph M. Hahn, Ph.D., an independent AI and machine learning consultant: jmh-datasciences.com · LinkedIn · [email protected]
Not investment advice. This is a research project and a demonstration of automated decisioning. Nothing here is a recommendation to buy or sell any security, and every backtested figure below is a hindsight upper bound rather than realized return.
License: writing and dashboards CC BY 4.0, free for any use including commercial; code and internal docs PolyForm Noncommercial, details
Author: Joe Hahn · Started: 2026-May-14 · branch: main
This project uses AI to manage a curated watchlist of tickers. You declare your goals, constraints, and an investment thesis (what you think will drive future returns), then initialize a starter watchlist you want exposure to. The solution is a three-stage pipeline with a deliberate division of labor, with only the middle stage making judgment calls:
So the AI decides which tickers the optimizer may choose among, and the math then decides the portfolio's weights. The results accumulate into dashboards where you can watch the watchlist composition, the recommended weights, and the realized portfolio value evolve over time, and monitor how realized gains are attributed to the news sources, authors, keywords, and waves behind each pick. In our experiments this coupling of AI curation with standard optimization significantly outperforms the optimizer on its own.
Who this helps. An investor who has a thesis about where markets are going but not enough time to track the news. This demo helps that investor move from a static buy-and-hold portfolio to one that is news-aware and lightly but effectively managed by AI. In the 3-year backtest described below, the AI-managed portfolio outperforms a buy-and-hold of a representative starter watchlist by a wide margin. The curator's job is to compound a thesis you already hold, not to invent one you don't.
Results are served via three families of GitHub Pages, with each showing results that are generated by the same retriever+curator pipeline playing across three different slices of time.
Backtest dashboards: these dashboards test this solution across 3 years of historical news gathered from July 2023 to 2026:
Bootstrap dashboards: these dashboards inspect outcomes as this solution focuses on the transition from using the last 3 months of the historical backtest news to news acquired daily since July 22, 2026:
Forwardtest dashboards: these dashboards playtest this solution on news ingested daily since 2026-07-22:
Each news ingest + portfolio rebalance follows one iteration of the following loop. The curator is the only part of this solution that relies on AI for judgment calls, everything else is deterministic Python.
The highlighted box is where AI provides an advantage to the buy/hold investor: an LLM reads the news against your thesis to decide which tickers the optimizer then gets to choose among. After that, a traditional math-only optimizer selects which tickers among those in the watchlist get funded, and how much.
See GLOSSARY.md for the finance terms used below (σ, μ, Σ, Sharpe ratio, risk aversion λ, mean-variance optimization, etc.). REFERENCE.md also contains many other project details (repo layout, code, input and output files, architecture, testing) and the granular details while this page provides the higher-level tour.
Install dependencies, copy the starter files, edit them to your taste, add your API keys, and then install the cron jobs:
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
cp examples/* .
examples/ holds a starter version of every file you configure, and the names match their destinations, so nothing needs renaming. None of the four are tracked at the repo root, so a fresh clone carries no personal data and no curated source lists; you copy the templates once and then edit them in place. Step 3 covers what goes in each.
API keys are the exception: they live in a .env file you create yourself, covered in step 4.
Everything you configure lives in six files at the repo root, and all six are gitignored, so nothing you write there reaches GitHub. Starter versions of each ship in examples/, which is tracked.
| File | Tracked | What you put in it |
|---|---|---|
investor_profile.md | no | Goals, wave thesis, exclusions, and every optimizer and curator knob |
holdings.csv | no | Your actual portfolio, ticker,shares |
.env | no | API keys |
gcp-key.json | no | Google BigQuery service-account credentials, is needed only if you want to rebuild the backtest news corpus |
news_sources.md | no | Which news domains to trust, block, and treat as specialty desks |
retrieval_config.json | no | Per-wave search keywords, used by the retriever |
investor_profile.md is where you state your investing goals, constraints, and exclusions, write the wave thesis the curator reasons against, and set every parameter this solution runs on. The knobs you will touch most are risk_aversion (λ, how hard the optimizer trades expected return against variance), max_watchlist_size (how many tickers the curator may keep in play), and news_lookback_days (how much recent news it reads at each rebalance).holdings.csv is a two-column CSV (ticker,shares) of your real positions. The template ships with placeholder rows showing the format; replace them with what you actually own, or set the shares to 0 to start from cash. Thereafter you edit holdings.csv only when you actually trade; the biweekly review manages watchlist.csv (the curator's universe, a single ticker column) for you, so that one is not a file you configure.news_sources.md is where you list the news sources you trust, in two tiers: source_major for the wire services and major outlets, and a prose section for the specialty, wave-specific desks. The two are ranked differently because the specialty desks cover a narrow wave in depth and also get their own dedicated search pass, so their reporting is not buried under the generic market wires. A third list, source_block, names domains to drop outright, the listicle and press-release mills. It ships pre-populated, so edit to taste.retrieval_config.json is where you tell the retriever what to look for. wave_keywords maps each wave to the phrases that surface an article, org_stoplist drops entities that are not companies, and engine holds a few tuning guards. Adding a wave to investor_profile.md does not feed retrieval on its own: you must add matching keywords here too. REFERENCE.md covers the retrieval engines themselves.One key is required. Put it in a .env file you create at the repo root (gitignored, no quotes, no export prefix), one KEY=value per line:
ANTHROPIC_API_KEY=... # required: an Anthropic LLM drives the daily web search
OPENROUTER_API_KEY=... # optional: only for non-Claude curator models
Any Claude model can drive the news pull, and any model can curate; this demo gets good results from claude-sonnet-5 as the curator and the cheaper claude-haiku-4-5 on the pull. Which key a run needs follows from the model named in investor_profile.md: a claude-* id routes through Anthropic, a vendor/model id routes through OpenRouter, so omit the OpenRouter line unless you name a non-Claude model. See REFERENCE.md for an additional BigQuery credential that will be required to execute this solution's backtest.
Open your crontab:
crontab -e
and add the following, editing the PWR_path line just once to your actual repository path:
# portfolio-wave-rider: set the repo path once; all four jobs below reuse it
PWR_path=/path/to/portfolio-wave-rider
# Forward news pull + RBS/RFT dashboard refreshes, every day incl. weekends, 18:30 local (after the US close)
30 18 * * * $PWR_path/scripts/news_pull.sh
# Weekday price snapshot + CBS/CFT dashboard refreshes, Mon-Fri 16:30 local
30 16 * * 1-5 $PWR_path/scripts/price_snapshot.sh
# CBS then CFT curation, every other Sunday 19:00 local
0 19 * * 0 $PWR_path/scripts/review_curation.sh
# CBS + CFT catch-up, Mon 08:00 local, only if Sunday was missed
0 8 * * 1 $PWR_path/scripts/review_curation.sh
Verify with crontab -l. Works the same on macOS and Linux, and leaves any other crontab entries untouched. cron makes the PWR_path variable available to every command below it, so it must appear before the job lines. The scripts ship executable, so no chmod is needed.
news_pull.sh, every day including weekends): the forward news pull plus two render-only dashboard refreshes. It runs your wave queries through Anthropic web_search and appends the raw articles to the frozen corpus under data/forward_corpus/, then re-renders the two retriever dashboards that visualize that corpus, the Retriever Bootstrap (RBS) and the Retriever Forwardtest (RFT). The evening slot (a few hours after the US close) captures the full day including after-hours earnings and evening coverage, freezing each article near its publication date, which keeps the corpus clean for later forward testing.price_snapshot.sh, Mon to Fri): the price snapshot plus two render-only dashboard refreshes. It snapshots per-ticker prices into data/snapshots.csv (well after the 16:00 ET close, so the daily close is final), then re-replays the two paper portfolios against those prices and re-renders them: the Curator Bootstrap (CBS) and the Curator Forwardtest (CFT, the landing page). Both run in this one job because they read the same-day snapshot; neither makes an LLM call.review_curation.sh): the self-gating rebalance curation, and the only cron step that calls the curator LLM. Both paper portfolios are curated by this one job, back to back and on the same every-other-Sunday schedule: CBS first, then CFT, which also writes a fresh recommendation report to data/reports/. Each fires only when a full rebalance_period has elapsed since that portfolio's last curation (biweekly at present), and each Sunday curation becomes the following Monday's rebalance, so the paper portfolios turn over the same morning you do. Sunday evening is the working slot: it lands after that day's 18:30 news pull, which runs on weekends too, so the curator reads same-day news and the refreshed dashboard and report are waiting before you rebalance Monday morning. The Monday 08:00 firing exists only to catch a Sunday the machine slept through; --if-due makes it a no-op otherwise, so the pair still costs one curator call per portfolio per period. The cadence lives in investor_profile.md's rebalance_period, so changing it needs no cron edit. Its phase, meaning which weekday the rebalances fall on, is the GRID_ANCHOR date at the top of review_curation.sh; it is a Sunday, and both paper portfolios share it so they curate on the same firing rather than on alternating weeks.All jobs append timestamped output to data/snapshot.log, and all tolerate failures so a web_search or price hiccup never blocks the rest.
cron only fires while the machine is awake and does not replay missed runs. The price snapshot heals itself within a week: each run backfills any session inside its 7-day price window that the file is missing, so a snapshot lost to a sleeping laptop or a rate-limited fetch is filled in by the next weekday run. Anything older than that window needs .venv/bin/python -m src.cli snapshot --date YYYY-MM-DD by hand. Note that a backfilled row is priced with the shares currently in holdings.csv, so it is only correct if the position did not change in between.
The snapshot refuses to write a date it cannot fully price. yfinance rate-limits by IP and returns an empty result rather than an error, so a partially priced day would otherwise record the missing positions as $0 and show up as a crash on every chart that reads total_value (this happened on 2026-08-31: three of four holdings were throttled out and the book read $16,392 against the prior close's $64,700). The fetch retries with backoff first, and a day that still cannot be priced is left absent for the next run to backfill. A missed news pull cannot be cleanly backfilled, because re-querying web_search about a past day reintroduces hindsight, so a laptop that sleeps through 18:30 leaves a gap in the corpus (recorded in the manifest at data/forward_corpus/pulls.jsonl).
To publish refreshed dashboards to GitHub Pages: git add docs/ && git commit -m "Refresh dashboards" && git push, since cron does not auto-push.
To uninstall: run crontab -e and delete the PWR_path line and the three job lines.
This project's portfolio-optimization activities:
Every weekday after the close, cron records what your portfolio is worth: per-ticker shares, close prices, and totals append to data/snapshots.csv, and the dashboards re-render off that fresh snapshot.
Once per rebalance period, the review acts: the curator reads the trailing news_lookback_days of news against your wave thesis and proposes adds and removes to the watchlist, the optimizer recomputes weights across the updated watchlist, and a report lands in data/reports/<date>-review-portfolio.md. Read it to see what the curator changed this period and what the optimizer now wants. The cadence is financial_model.rebalance_period in investor_profile.md, so change it there rather than in your crontab.
The optimizer's output is appended to data/recommendations.csv, and nothing is traded; see Acting on a recommendation below.
A backtest replays the whole pipeline across the historical window named in investor_profile.md. At each rebalance the curator sees only the news that existed on that date, proposes adds and removes against your wave thesis, and the optimizer recomputes weights for whatever watchlist results. The run then measures the portfolio against two baselines, a buy-and-hold of your starter watchlist and SPY, so what you learn is whether curation added anything the market would not have handed you anyway. Results from our own backtest are displayed via two dashboards: RBT for what the retriever surfaced across the window, and CBT for what the curator did with it and how the portfolio fared.
A sweep re-runs the pipeline across a grid of parameter values and ranks the outcomes, so you can see how sensitive the result is to a setting instead of trusting the one you happened to pick. The optimizer knobs (concentration_cap, risk aversion λ, the price lookback) touch only the mean-variance replay, so an entire grid costs nothing but local compute, no LLM calls and no news re-fetch. Curator knobs are different: max_watchlist_size shapes the curator's decisions, so each value needs its own set of curator calls. Results from our sweeps are displayed in the SBT dashboard.
The review report ends with recommended weights, not trades. The project never touches your brokerage. To act on a recommendation:
concentration_cap); decide which subset you actually want to execute.holdings.csv with the new share counts to match your brokerage.You can also do nothing and let the next review produce a fresh recommendation. The split between recommendation and execution is intentional so you can review, override, or ignore each call.
holdings.csv and watchlist.csv shape outcomesTwo files describe the portfolio, with a clean split of ownership:
holdings.csv (ticker,shares) is what you actually own — real positions, shares > 0. You edit it, and only you, after executing trades in your brokerage; the curator/cron never writes it. It drives the snapshots, current allocation, and the "sell/current" side of trade recommendations.watchlist.csv (single ticker column) is the curator-managed universe — the tickers the optimizer may assign weight to. The biweekly review auto-adds and auto-removes here and you normally do not touch it, though appending a ticker by hand would be how one could manually put a new ticker on the curator's radar.The optimizer's universe is the combination of three lists: every ticker in watchlist.csv, every ticker you currently hold in holdings.csv, and the profile's always_include anchors. It can weight nothing outside that combined list. Consequences:
watchlist.csv while you still hold it, it stays in the universe because you still hold it, so the optimizer can recommend selling it; it leaves the universe only once you sell it out of holdings.csv.always_include, they sit outside max_watchlist_size, and they are never in watchlist.csv, so the curator cannot add or remove them. This guarantees the optimizer always has safe-haven tickers available to fund, though nothing forces it to.data/curation_history.csv.The split is deliberate. Python does everything deterministic; an LLM does the one judgment call.
src/portfolio.py; the news retriever and the corpus it writes to are src/retriever.py and src/corpus.py. You run all of it through the CLI (python -m src.cli <subcommand>) and a few thin cron scripts.The curator decides which tickers belong on the watchlist. Its job is composition only: read the news, decide what to add and remove against the current watchlist. It never proposes weights or forecasts; it manages the set of tickers the optimizer may choose among, informed by recent news and in a way that is aligned with your investment thesis.
On each call the curator:
investor_profile.md and the current watchlist from watchlist.csv.news_sources.md.max_watchlist_size, each cited to dated news.Python then validates that payload, and only the changes that survive touch watchlist.csv; your real positions in holdings.csv are never modified. REFERENCE.md has the validation rules.
The optimizer used here selects a portfolio by maximizing this mean-variance objective function:
μᵀw − λ·wᵀΣw
subject to ∑ᵢ wᵢ = 1 (weights sum to one) and 0 ≤ wᵢ ≤ concentration_cap. The first term μᵀw is the portfolio's expected return (the weighted average of per-ticker expected returns); the second term wᵀΣw is the portfolio's return variance, scaled by λ to act as a risk penalty. μ is the per-ticker expected-return vector, computed as the annualized mean of daily log returns over a trailing price-history window (optimizer_lookback_days, set in investor_profile.md). Σ is the ticker × ticker covariance matrix estimated over the same window. w is the weight vector the optimizer is solving for. λ (risk aversion) trades expected return against variance:
λ → 0: the solution favors high-return tickers that tend to have greater variability.λ: leans toward higher-reward tickers while keeping a real variance penalty. The live value is set in investor_profile.md under financial_model.risk_aversion.λ ≫ 1: the variance penalty dominates, so the solution concentrates in the lowest-variance tickers available, typically the bond and T-bill anchors.This is the standard Markowitz mean-variance formulation (Markowitz 1952, Portfolio Selection, Journal of Finance 7:77-91), which is the textbook starting point for portfolio construction because it captures the central return-vs-risk tradeoff in a single quadratic expression. See GLOSSARY.md for the full definitions.
Provisional. The forward test that would settle the central question is still accumulating, so what follows is what the backtest shows, not a result to lean on.
The backtest runs biweekly across 2023 to 2026 against an equal-weight [AAPL, GOOGL, AMZN] buy-and-hold investor too busy to track the news; we know such investors exist because the author is one. The curator proves disciplined, swapping rarely and only on a concrete catalyst, every ticker real and US-listed. The optimizer then concentrates into whichever watchlisted tickers have the strongest recent returns, and the AI-managed book beats that buy-and-hold by a wide margin.
Where the lift comes from, and why to distrust it. Most of the gain rides on a single dominant position: one favorable wave caught and held is not a broad-based edge, and a single winning bet (n=1) cannot separate skill from luck. The result is also in-sample. The date-clean retriever removes the retrieval leak, so the curator only ever saw period-correct news, but the curator is an LLM whose training postdates the window and may simply remember which names won. Treat the backtest as a hindsight-tinted upper bound. The only honest test is forward testing: hold the config fixed and measure performance on quarters that postdate the training cutoff. That forward testing is happening now.
This project was developed with Claude Code. See CLAUDE.md for the rules Claude follows when operating in this repo. CLI flags, repo layout, output files, architecture overview, and testing instructions live in REFERENCE.md. Finance and stats terms are defined in GLOSSARY.md.
I am Joseph M. Hahn, Ph.D., an independent AI and machine learning consultant. Through JMH DataSciences I build production AI and machine learning systems for clients who need a real decision automated, rather than a demo. Before going independent I spent eight years inside Oracle's AI Center of Excellence delivering AI systems for enterprise clients in manufacturing, oil and gas, public sector, and retail, and before that four years building machine learning systems on large data platforms.
This repo is one of several demonstrations of the same underlying pattern: an AI reads a stream of unstructured input and automates a routine decision, while deterministic code handles whatever has to be auditable. The hard part is rarely the model. It is drawing the line between the judgment worth delegating and the arithmetic that has to stay reproducible, and then building the scaffolding that proves the system is not fooling itself, which is most of what the Findings section above is about. If that shape matches a problem in your business, the work I do and what it costs are at jmh-datasciences.com.
Related work:
Technical demo. Not financial advice. Historical performance is not predictive. Do not trade real money on this output without independent verification.
This repository is dual-licensed, split by what a file is for.
| What | License | Commercial use |
|---|---|---|
The public writeup and the published pages: README.md, GLOSSARY.md, and everything under docs/ (the rendered dashboards and the figures they display) | CC BY 4.0 | Yes, with attribution |
Everything else: src/, scripts/, data/, config, and the internal docs REFERENCE.md and CLAUDE.md | PolyForm Noncommercial 1.0.0 | No, ask |
The writeup and the dashboards are meant to travel. Quote them, adapt them, screenshot a chart into your slide deck, commercially or not, as long as you credit the source. The requested form is Joseph M. Hahn, Ph.D., JMH DataSciences, https://jmh-datasciences.com, from the portfolio-wave-rider project.
The code and the internal docs are free to use, modify, and share for any noncommercial purpose: research, experimentation, education, personal projects, and use by nonprofit or government organizations. Commercial rights to them are reserved. If you want to use this work commercially, or want it adapted to a decision in your own business, get in touch at jmh-datasciences.com or [email protected] and we will talk.
Code samples embedded in a CC BY document stay under the code license, so a snippet lifted from this README carries the same terms as the file it came from.
Third-party content. data/ and parts of the dashboards contain headlines, snippets, and URLs from published news articles. That material belongs to its publishers, is included here for research and commentary, and is not licensed by the author under either license above.
Effective 2026-08-31; the PolyForm terms took effect 2026-07-30. Versions published before 2026-07-30 were MIT and remain MIT for anyone who obtained a copy under it; the terms above govern this commit onward.