Skip to content
HaoweiChanPublic

About

Open-source quantamental portfolio engine: multi-factor screening, LLM agent research, and IPS-driven rebalancing proposals. It ranks, argues, and proposes — it never places an order.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Kuroshio

Kuroshio

TradingAgents for your whole portfolio — run it on your holdings daily, not one ticker once.

CI License Python Ruff

▶ Live demo — real screener, proposal and backtest output from one session, frozen into a single static page.

Kuroshio demo — the screener ranking a 50-name universe on a real session

Named after the Kuroshio — the Pacific current that carries apex predators past Taiwan.

Kuroshio is an open-source quantamental portfolio engine:

  • Screen — multi-factor momentum screeners (US, TW) with cross-sectional percentile scoring
  • Research — LLM agent research per holding, facet-cached so daily runs cost cents, not dollars
  • Propose — IPS-driven challenger-vs-incumbent rebalancing proposals (the engine never executes trades)

Status: 0.x. Usable today; the API can still change between minor versions.

Quickstart

pip install -e ".[yfinance,dev]"

# rank breakout candidates (zero API keys — yfinance default)
kuroshio screen --market us --tickers NVDA,AMD,AVGO,MSFT,META,LLY,XOM,JPM,COST,NFLX

# check a policy file
kuroshio ips-validate examples/ips-balanced.md

# proposal cards: your holdings vs challengers, governed by your IPS
printf -- '- {ticker: AAPL, weight: 0.08, theme: tech, score: 0.55}\n- {ticker: XOM, weight: 0.22, theme: energy, score: 0.30}\n' > holdings.yml
kuroshio propose --ips examples/ips-balanced.md --holdings holdings.yml --market us

us is 12-1 momentum since 2026-09 (the less bad of the two US screens — neither beats SPY out of sample, see docs/backtest-2026-09.md); the previous MA200/breakout leadership screen is still there as --market us-leadership.

score: is optional (same for final_score: in a candidates file) — leave it out and propose fetches prices from the market's provider and fills it in. Hand-typed values always win, per name. An auto-filled score is a percentile rank within the names in your own files, not the number kuroshio screen prints for that name: the pool is your holdings + candidates, not a universe, and, for us-leadership, propose passes no --sector-map/--asof. Pass --universe-file members.csv (a newline ticker list, or a date,tickers snapshot from scripts/sp500_members.py) to rank against that index instead, and the card discloses it did. Two things follow. A pool small enough that your turnover hurdle may not be doing any work gets nothing filled at all — you get the "no holding has a screener score" ALERT instead of a made-up gap. (That floor is a deliberately conservative rule of thumb, not an exact cutoff: it will sometimes refuse a pool your hurdle could have judged fine. Hand-type the scores, or list more names.) Above that size the score is filled, but percentile ranks pin their extremes to 0.000 and 1.000 however tightly the names cluster, so every card built from one says which scores were filled and how many names they were ranked against — and, when the other side of the swap is hand-typed, that the gap spans two different scales.

Record why you own a position — setup_type: (value_dip | pullback_add | trend_add | other) plus entry_price: and, for the dip setups, invalidation_price: — and propose watches each one on its own terms: a trend_add is alerted when it closes more than 3% under its 50-day mean, a value_dip never is (looking weak against its MAs is the setup) and is alerted only when it closes at or below the invalidation price you recorded. Add an entry_date: and the stop moves with the tape: a trend_add — and a pullback_add once its running high has cleared entry + 2R — trails its invalidation price at caps.trail_atr_mult ATR14s below the running high since entry (default 3), never downward. On top of that, one rule reads no setup at all: a position whose worst close since entry_date is at or past caps.max_adverse_excursion_pct from its entry (default -15%) gets a DECIDE card — kill it, add to it per the plan, or rewrite the thesis. A position through the exit level it was opened with (its invalidation price, the ratcheted stop, or a trend_add's 50-day line) gets an EXIT card instead: the plan was written in advance, so the card says sell it all rather than asking. Holding it unchanged is not one of the three. Positions missing the fields a rule reads are named on a card instead of quietly going unwatched. This applies only to a session that actually fetched prices: a book with no monitored setup_type, no entry_price, and no caps.book_vol_target_pct needs no price and never fetches one, so it is not "blind" — no missing-price ALERT, exit 0. When a fetch did happen and a held position came back with no price (a rate limit, say), that is not "unwatched by design" either — it gets its own missing-price ALERT naming it, separate from the coverage line, and if every held position came back unpriced the run was blind: propose exits 3 rather than 0 (cards still print; --help documents the exit codes, 2 being an invalid IPS or a bad holdings/candidates/ universe/provider input). Every ratchet move is appended to the stop ledger so evaluate can score the stop that was live on a date, not just the final one; --no-ledger (like screen and research) still reads that ledger for the never-lower rule but appends nothing, and says on stderr how many moves went unrecorded — for a dry run, a probe, or the desk previewing a book without committing state.

A held name's newest kuroshio research rating is a veto, never a ranking input: rating hit rate is unmeasured until evaluate has 60+ sessions, so it never enters the swap hurdle or the verdict floor that judge challengers. Instead, a rating at or below Underweight forces the same three-way DECIDE as MAE — kill it, rewrite the thesis, or hold with a written reason — quoting the rating, its date, source and model, and the stop the report gave. It reads no price, so a holding with no session price still gets the card; a rating an earnings print has since voided (the same rule kuroshio book applies) does not count; and when MAE also fires on the same name this run, the two fold into one card rather than two.

Run propose twice a day — once on a book (the target this project computes) and once on the actual portfolio: the broker-side holdings file the desk itself writes, run with propose --candidates <book>/candidates.yml so the book's names — written there by kuroshio book, one core+attack ticker/score/verdict row each — stand in as challengers. The ratchet stop, thesis break, MAE and DECIDE cards then run on what is really held, not the model. That file's entry_date can be a snapshot's tracking start rather than a real fill, so it carries entry_date_source: manifest_first_seen (a fill — unchanged) or snapshot_first_seen: propose drops entry_date for the latter (a running high measured from a tracking start would ratchet the stop above the entry and breach on day one) and names the ticker on the coverage line instead, so it reads as "not fully monitored" rather than silently going unwatched.

screen prints a ranked table; propose prints cards like this — every one of them cites the IPS clause that triggered it:

### SWAP TSLA → GE

Challenger GE scores 0.792 vs incumbent TSLA's 0.233 — a gap of 0.559, above your IPS turnover
hurdle of 0.150 plus estimated round-trip friction of 0.020%. GE's verdict is 'neutral', at or
above your floor of 'neutral'.
- score gap: +0.559
- est. friction: 0.020%
- per your IPS: turnover.hurdle, turnover.verdict_floor, friction.us_roundtrip_pct

Every card body renders in the IPS lang field (en default) — pass --lang zh (or zh-TW/zh_TW) to propose/book to override it, in Traditional Chinese, per card; tickers, theme names, setup_type values, IPS clause keys, dates and numbers stay verbatim in either language.

That is one card out of the five the same run produced. The whole set, rendered on the demo page — a theme-budget breach, a hard-cap trim, two swaps, and the swap the weekly turnover limit suppressed:

Five proposal cards: an ai-semis theme ALERT, a TRIM on NVDA, SWAP TSLA to GE, SWAP PG to BAC, and an ALERT for the swap suppressed by the weekly turnover cap

Full walkthrough (including the LLM research pipeline): examples/quickstart.md.

kuroshio book turns files you already have into a mechanical book: a screen ranking (kuroshio screen --json), your ratings ledger, and your IPS become holdings.yml, candidates.yml, book.json, book.md, alloc.md, needs_research.json and propose.out in the directory you name. holdings.yml is the handoff file for whatever runs your real book: every name carries setup_type, the report's invalidation_price, entry_price/entry_date and a one-line thesis, so a desk that copies those fields into its own holdings file gets the same stop, trend and loss rules propose applies here — its fields are an interface, kept stable by tests. candidates.yml is the book's core+attack names, ticker/score/verdict only — the file the actual-portfolio pass above reads with --candidates. It walks the ranking under a per-theme cap, drops names a rating vetoed or a TTL/earnings print expired, sizes each one at min(base, caps.position_pct, percent-risk) times any PM multiplier, spends a default 30% attack budget on theme-cap overflow sized at twice base and twice the risk budget (with the same position cap and PM multiplier, shrunk to remaining attack/theme room), then uses the remainder to double core names by rank up to twice base. Both overflow and core top-ups require a rating at or above attack_floor (default overweight); unused budget stays in cash. With --nav and an optional symbol,quantity,market_value,average_price positions export, it sizes the whole thing in money against what you already hold. Every input is an option and every rule is a flag; nothing is read from a fixed location, so your holdings, NAV and reports stay outside the repo.

Concentration is the default. Optional --leverage-map FILE supplies a flat YAML UNDERLYING: ETF_TICKER map: mapped attack rows (overflow and topped-up core) buy 2x ETFs at unchanged capital weight. All caps still charge capital; exposure and loss-at-stop are about twice that amount, and daily resets drift from 2x over multi-day holds. Tickers, stops and targets remain the underlying. ETF shares use the book-date close from the screen or existing --provider history; without a quote shares are n/a and allocation dollars reserve target capital before rounding. holdings.yml adds optional vehicle and exposure metadata; its existing leverage field retains propose's cap-charging semantics and is not set by this map. propose accepts the metadata but still evaluates underlying prices and capital weights.

The output is a mechanical consequence of your files, not investment advice, and the engine still never places an order.

kuroshio site --book <dir> --reports <dir> --out <dir> renders that book directory, plus any kuroshio research report tree, as a static site in this project's design system: a book page, an allocation page, a report index and one page per report, relative links only, output directory swapped in atomically so a server never serves a half-built tree. Labels come from a table keyed by --lang (en or zh, defaulting to your IPS lang field), and the stylesheet is the same docs/style.css this project's own page uses. Needs pip install -e ".[site]". Same disclaimer: mechanical output, not advice.

kuroshio research reasons over a paid LLM API. kuroshio mcp (pip install -e ".[mcp]") is the free alternative: a stdio MCP server that exposes the engine's data tools plus screen/propose/record_rating, wired up via .mcp.json (paths there are relative to the repo root — adjust command if your checkout lives elsewhere) so a Claude Code session can do the reasoning itself and pay nothing beyond its own subscription; see .claude/skills/research/SKILL.md and "Session mode (MCP)" in docs/ARCHITECTURE.md.

Whenever kuroshio research writes a report tree's 5_portfolio/decision.md, it also writes decision.json beside it — the same rating/stop-loss/price-target the ratings ledger gets (even with --no-ledger), plus the Executive Summary and Investment Thesis sections verbatim, so a downstream job can turn a run straight into a thesis card without scraping markdown.

Architecture

flowchart LR
    P["providers/<br/><small>yfinance · FinMind</small>"] --> S["core/screening/<br/><small>gates → pctrank</small>"]
    A["agents/engine/<br/><small>LLM debate · facets</small>"] --> V["verdicts<br/><small>buy · neutral · sell</small>"]
    I["your IPS.md<br/><small>caps · hurdle</small>"] --> AL
    S --> AL["core/allocator/<br/><small>challenger vs<br/>incumbent</small>"]
    V --> AL
    AL --> C["ProposalCard<br/><small>EXIT · SWAP · TRIM<br/>DECIDE · ALERT</small>"]
    C --> O["CLI · Discord"]
Loading

Core logic never imports a data vendor: providers are plugins, screening and allocation are pure functions over plain dataclasses, and every edge is an adapter at the boundary. Details in docs/ARCHITECTURE.md; adding a market is one module plus one registry entry, see docs/adding-a-market.md. Shipped and planned work: docs/ROADMAP.md.

Maintenance & support

Self-hosted engine, provided as-is — no SLA. Issues and PRs are triaged against the maintainer's roadmap on maintainer time, not on demand; see CONTRIBUTING.md. Security reports are the exception and always take priority; see SECURITY.md.

License

Apache 2.0. Includes components derived from TradingAgents (Apache 2.0) — see NOTICE.

Disclaimer

Kuroshio is a software tool. It produces research reports and rule-triggered proposals from your own rules (IPS) and your own API keys. It is not investment advice, and it never places orders. Any figures in this repo or on the demo page are illustrative output from a fixed ticker list on one session — not live data, not a recommendation, and not a track record.

About

Open-source quantamental portfolio engine: multi-factor screening, LLM agent research, and IPS-driven rebalancing proposals. It ranks, argues, and proposes — it never places an order.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages