This repository is a multi-agent international equity research system. It can analyze single tickers, run broader screening pipelines, and optionally reconcile saved results against an Interactive Brokers portfolio through either a CLI workflow or a local Flask dashboard.
You need Python 3.12+, Poetry, and at least one working LLM API key. A Gemini key is the minimum practical setup; optional data-provider keys improve coverage and reliability.
- Multi-agent international equity analysis for individual tickers
- Structured markdown reports and charts
- Screening pipeline for broader exchange-wide discovery
- Optional IBKR portfolio reconciliation and watchlist handling
- Optional local Flask dashboard for portfolio and refresh monitoring
This is not a single prompt wrapped in a CLI. The runtime fans out work across specialist agents, applies deterministic validation before debate, then routes the surviving analysis into valuation, risk, and portfolio decision stages.
graph TB
Start(["User: Analyze TICKER"]) --> Dispatcher{"Parallel<br/>Dispatch"}
Dispatcher --> MarketAnalyst["Market Analyst<br/>(Technical)"]
Dispatcher --> SentimentAnalyst["Sentiment Analyst<br/>(Social)"]
Dispatcher --> NewsAnalyst["News Analyst<br/>(Events)"]
Dispatcher --> JuniorFund["Junior Fundamentals<br/>(API Data)"]
Dispatcher --> ForeignLang["Foreign Language<br/>(Native Sources)"]
Dispatcher --> LegalCounsel["Legal Counsel<br/>(Tax & Reg)"]
Dispatcher --> ValueTrap["Value Trap Detector<br/>(Governance)"]
Dispatcher -.-> Auditor["Forensic Auditor<br/>(Independent Check)<br/>Optional"]
MarketAnalyst --> SyncCheck["Sync Check<br/>(Fan-In Barrier)"]
SentimentAnalyst --> SyncCheck
NewsAnalyst --> SyncCheck
ValueTrap --> SyncCheck
Auditor -.-> SyncCheck
JuniorFund --> FundSync["Fundamentals<br/>Sync"]
ForeignLang --> FundSync
LegalCounsel --> FundSync
FundSync --> SeniorFund["Senior Fundamentals<br/>(Scoring)"]
SeniorFund --> Validator["Financial Validator<br/>(Red-Flag Detection)"]
Validator --> SyncCheck
SyncCheck -->|"REJECT"| PMFastFail["PM Fast-Fail<br/>(Skip Debate)"]
SyncCheck -->|"PASS"| DebateR1{"Parallel<br/>Debate R1"}
DebateR1 --> BullR1["Bull Researcher R1"]
DebateR1 --> BearR1["Bear Researcher R1"]
BullR1 --> DebateSyncR1["Debate Sync R1"]
BearR1 --> DebateSyncR1
DebateSyncR1 -->|"Normal"| DebateR2{"Parallel<br/>Debate R2"}
DebateSyncR1 -->|"Quick"| DebateSyncFinal["Debate Sync Final"]
DebateR2 --> BullR2["Bull Researcher R2"]
DebateR2 --> BearR2["Bear Researcher R2"]
BullR2 --> DebateSyncFinal
BearR2 --> DebateSyncFinal
DebateSyncFinal --> ResearchManager["Research Manager<br/>(Synthesis)"]
ResearchManager --> ValuationCalc["Valuation Calculator"]
ResearchManager -.-> Consultant["External Consultant<br/>(Cross-Validation)"]
Auditor -.->|Independent Forensic Report| Consultant
ValuationCalc --> Trader["Trader<br/>(Plan)"]
Consultant -.-> Trader
Trader --> RiskyAnalyst["Risky Analyst"]
Trader --> SafeAnalyst["Safe Analyst"]
Trader --> NeutralAnalyst["Neutral Analyst"]
RiskyAnalyst --> PortfolioManager["Portfolio Manager<br/>(Verdict)"]
SafeAnalyst --> PortfolioManager
NeutralAnalyst --> PortfolioManager
PMFastFail --> ChartGen["Chart Generator"]
PortfolioManager --> ChartGen
ChartGen --> Decision(["BUY / SELL / HOLD"])
style Dispatcher fill:#ffeaa7,color:#333
style SyncCheck fill:#e0e0e0,color:#333
style Validator fill:#ffcccc,color:#333
style Consultant fill:#e8daff,color:#333
style Auditor fill:#e8daff,color:#333,stroke-dasharray: 5 5
style PMFastFail fill:#ffcccc,color:#333
style Decision fill:#55efc4,color:#333
At a high level:
- Parallel analyst fan-out gathers market, news, sentiment, fundamentals, language, legal, and value-trap evidence.
- Fundamentals are synthesized and then checked by deterministic red-flag rules before the debate path is allowed to continue.
- Bull and bear researchers argue one or two rounds depending on
--quick, and Research Manager consolidates the result. - Valuation, trader, and risk personas shape the portfolio decision before Portfolio Manager emits the final verdict.
- Chart generation and report rendering run after the decision.
- Memory and retrospective context are optional layers around the core analysis flow, not substitutes for it.
git clone https://github.com/rgoerwit/ai-investment-agent.git
cd ai-investment-agent
poetry install
cp .env.example .envAt minimum, set GOOGLE_API_KEY in .env. Optional keys such as Tavily, FMP, EODHD, and OpenAI improve coverage, search quality, or optional consultant paths.
poetry run python -m src.main --ticker 7203.T --quick --output results/7203.T.mdThat command exercises the main runtime and writes a markdown report. Saved analysis JSONs in results/ are also what later power portfolio_manager.py and the dashboard.
- Analyze one ticker: use
python -m src.main --ticker ... - Screen a broader universe: use
scripts/run_pipeline.shwith or withoutscripts/find_gems.py - Reconcile a portfolio: use
scripts/portfolio_manager.py - Use the browser UI: run
python -m src.web.ibkr_dashboard.app, and start the worker only if you want queued refresh jobs
This is the core engine. Use it first before touching portfolio workflows or the dashboard.
# Normal run
poetry run python -m src.main --ticker 0005.HK
# Save markdown output and charts
poetry run python -m src.main --ticker 0005.HK --output results/0005.HK.md
# Faster first pass
poetry run python -m src.main --ticker 0005.HK --quick --output results/0005.HK_quick.md
# Stateless run without Chroma-backed memory
poetry run python -m src.main --ticker 0005.HK --no-memory --output results/0005.HK.mdPractical notes:
--quickis usually the right first-pass setting for screening or broad review.--outputis the cleanest way to get markdown plus chart assets in a stable location.- Free-tier Gemini works, but it is slow for larger batches. Paid tiers mostly improve throughput and reduce retry friction.
The screening pipeline is the shortest path from broad discovery to a shortlist of full reports.
# End-to-end path: scrape configured exchanges, filter, quick-screen, then run
# full analysis on BUY names only
./scripts/run_pipeline.sh
# Step-by-step alternative
poetry run python scripts/find_gems.py --output scratch/gems.txt
./scripts/run_pipeline.sh --skip-scrape scratch/gems.txtOutputs land in scratch/. In practice you will see:
- a source ticker list such as
gems_YYYY-MM-DD.txt - quick-screen outputs
- a
buys_YYYY-MM-DD.txtlist - full reports for BUY names
Resumption is built in:
- Re-running the same command family skips completed outputs.
- If Stage 2 was interrupted and you need to resume from an earlier day, point
--buys-fileat the originalscratch/buys_YYYY-MM-DD.txt. - If you already have your own ticker list, skip scraping and feed it directly to the pipeline.
scripts/portfolio_manager.py sits on top of the saved analysis JSONs in results/. It bridges the evaluator output with live or offline portfolio context.
# Verify credentials and IBKR connectivity first
poetry run python scripts/portfolio_manager.py --test-auth
# Report only, using saved results with no IBKR connection
poetry run python scripts/portfolio_manager.py --read-only
# Reconcile against live IBKR positions
poetry run python scripts/portfolio_manager.py
# Add order-size recommendations
poetry run python scripts/portfolio_manager.py --recommend
# Re-run stale analyses, then reconcile
poetry run python scripts/portfolio_manager.py --refresh-stale --quick
# Evaluate a specific IBKR watchlist against existing analyses
poetry run python scripts/portfolio_manager.py --recommend --watchlist-name "my watchlist"Notes:
--read-onlyis the safest way to understand the tool before you touch live broker data.--recommendproduces actionable suggestions and sizing guidance. Order execution is currently disabled, so the tool remains advisory.- Concentration warnings, stale-analysis flags, cash timing, and macro-demoted review items are part of the normal report output.
The dashboard is a local browser view over the same recommendation and reconciliation stack. It is useful once you already have analysis JSONs in results/.
# App only
poetry run python -m src.web.ibkr_dashboard.app
# Worker, only needed for queued background refresh jobs
poetry run python -m src.web.ibkr_dashboard.worker
# Live broker mode with an explicit account and watchlist
poetry run python -m src.web.ibkr_dashboard.app \
--account-id U20958465 \
--watchlist-name "default watchlist"
# Offline/read-only mode for saved results only
poetry run python -m src.web.ibkr_dashboard.app --read-onlyOpen http://127.0.0.1:5050.
Convenience options:
# Start both processes together
./scripts/run_ibkr_dashboard.sh
# Start only the Flask app through the launcher
./scripts/run_ibkr_dashboard.sh --no-worker
# Pass startup flags through to the app
./scripts/run_ibkr_dashboard.sh -- --account-id U20958465 --watchlist-name "default watchlist"If you have already run poetry install, the Poetry script shims also work:
poetry run ibkr-dashboard
poetry run ibkr-dashboard-workerThe dashboard includes:
- Overview: NLV, cash, freshness, pending inflows, concentration, portfolio health, macro alert
- Actions: stop breaches, sells, soft rejections, macro reviews, adds, trims, dip-watch candidates, holds
- Watchlist: new buys, off-watchlist candidates, monitor, and remove buckets
- Orders & Cash: live orders plus settlement timing
- Refresh: freshness summary and explicit background refresh jobs
- Settings: lightweight local preferences/stubs
Operational notes:
- The dashboard is read-only for trading.
- Live IBKR mode is the default. Use
--read-onlyorIBKR_DASHBOARD_READ_ONLY=truewhen you want a saved-results-only snapshot. - Set the account explicitly with
--account-idorIBKR_DASHBOARD_ACCOUNT_IDwhen the default IBKR account is not the one you want. - Set the watchlist explicitly with
--watchlist-nameor in the Settings tab. Startup flags win for that run even if saved dashboard preferences differ. - The page auto-loads a snapshot on first open.
Refresh Snapshotis the manual force-reload control. - Live orders and live broker cash context only appear in live mode.
- The dashboard process serves cached snapshot reads; the worker is the only process that executes queued refresh jobs.
- The module entrypoints are the most robust launch path because they do not depend on Poetry having installed wrapper scripts into
.venv/bin. - Saving settings only reloads the snapshot when the changed fields actually affect the bundle, such as account, watchlist, mode, or max-age.
- A snapshot status like
ready, read-onlywithFresh count > 0andNo refresh jobs yetis normal in offline mode. It means the dashboard successfully loaded saved analyses fromresults/, found nothing stale enough to queue automatically, and has not been asked to run any manual background job yet. - If all analyses are fresh, the stale/due-soon refresh buttons stay disabled. Use a ticker list if you want to force a rerun of specific names.
- While the Refresh tab is open, the UI polls
/api/refresh/jobsevery 5 seconds. In the Flask dev server logs that will look like repeatedGET /api/refresh/jobs 200lines; that is expected.
The built-in screen is looking for transitional value-to-growth or GARP-style opportunities, not momentum chasing.
Hard requirements:
- Financial health score of at least 50%
- Growth score of at least 50%
- Liquidity of at least about $500k USD daily
- Low enough analyst coverage to still be plausibly underfollowed
Soft factors that still matter:
- value-trap and governance warnings
- regulatory and jurisdiction risk
- capital allocation quality
- valuation stretch versus thesis quality
- business mix and US revenue exposure where relevant
Deterministic red-flag logic can reject a name before the debate path continues. That is intentional.
prompts/ Versioned prompt JSON files
scripts/ Screening, portfolio, and operator scripts
src/main.py Main CLI/runtime entrypoint
src/graph/ Graph assembly, routing, barriers
src/agents/ Node logic and shared agent state
src/tools/ Tool implementations by domain
src/toolkit.py Tool facade used by agent roles
src/data/ Market and fundamental data fetching
src/validators/ Deterministic validation and red-flag screening
src/charts/ Chart extraction and rendering
src/memory.py Chroma-backed memory and macro-event support
src/ibkr/ Portfolio, reconciliation, and broker integration
src/web/ibkr_dashboard/ Local Flask dashboard
tests/ Unit and integration coverage
How the pieces connect:
src/main.pyis the staged runtime entrypoint.src/graph/wires the workflow,src/agents/owns the node logic, andsrc/tools/plussrc/toolkit.pyprovide the tool surface.src/data/,src/validators/,src/memory.py, andsrc/charts/are shared subsystems used by the main analysis path.src/ibkr/andsrc/web/ibkr_dashboard/are adjacent operator workflows built on top of saved analysis outputs and, optionally, live broker context.
# Full suite
poetry run pytest tests/ -v
# IBKR-focused changes
poetry run pytest tests/ibkr -v
# Dashboard-focused changes
poetry run pytest tests/web -vIf you are changing core runtime behavior, run the full suite before you call it done.
Poetry or import issues
poetry env remove --all
poetry installIf poetry run ibkr-dashboard or poetry run ibkr-dashboard-worker warns that the entry point "isn't installed as a script", the commands were added to pyproject.toml after the virtualenv was created, or the project root was not reinstalled. poetry install fixes that. As a fallback, run:
poetry run python -m src.web.ibkr_dashboard.app
poetry run python -m src.web.ibkr_dashboard.workerPython version mismatch
- This repo expects Python 3.12.x.
- Check with
python --versionand make sure Poetry is using the same interpreter.
API errors or quota issues
- Check
.envfirst. - Free-tier Gemini works, but rate limits and retries are normal.
- If you have a paid tier, make sure the API key belongs to the right project and that your RPM settings in
.envmake sense.
portfolio_manager.py or analysis index rebuild is unexpectedly slow on macOS
Spotlight indexing on .venv/ or results/ can turn a normal index rebuild into a very slow one.
touch .venv/.metadata_never_index results/.metadata_never_indexThese are real features, but they are not required to get started:
- Container mode: the repo includes a Dockerfile and supports local bind-mounted runs. Prefer Podman if you want stronger workstation isolation.
- Observability: Langfuse and LangSmith hooks exist for tracing and diagnostics. For sensitive deployments, LangSmith also supports
LANGSMITH_HIDE_INPUTSandLANGSMITH_HIDE_OUTPUTS. - Inspection and tool audit hooks: see
src/tooling/if you want to inspect or audit untrusted external content before it reaches LLM context. - Deployment references:
terraform/contains reference infrastructure, not a turnkey hosted product. - Dependency note:
yfinance 1.2.0still pinscurl-cffi <0.14upstream. The repo tracks the current SSRF advisory and currently treats it as a constrained transitive risk because Yahoo data paths here are driven by ticker-like symbols, not attacker-controlled URLs.
- This is a research tool, not an automated trading system.
- Data quality and coverage vary by provider, exchange, and ticker.
- Forward catalysts and regime changes are harder than backward-looking financial analysis.
- Broad screens can be slow on free-tier APIs.
- Portfolio workflows depend on having saved analysis JSONs in
results/.
Contributions are welcome. Good targets include:
- additional or higher-quality data sources
- validator and data-pipeline hardening
- IBKR and portfolio workflow improvements
- Flask dashboard enhancements in
src/web/ibkr_dashboard/, including drilldowns, settings, monitoring, and presentation - test coverage and documentation cleanup
For orientation, start with:
AGENTS.mddocs/CODEBASE_MEMORY.md- this README
License: MIT
Disclaimer: This system is for research and educational use. It is not financial advice.
- LangGraph and the broader LangChain ecosystem for the orchestration substrate
- Open-source data and infrastructure tools that make local-first experimentation practical