Extension Guide (Cookbook)
"I want to add X" — which files to modify, organized by real-world scenarios. Each entry provides specific paths + estimated scope of changes.
← 06-api · Wiki Index · 08-deployment →
Table of Contents
- Add a New Asset
- Add a New Data Source
- Add a New Agent Role
- Add a New Connector (e.g., Telegram Bot)
- Add a New Cron Job
- Add a New Web Endpoint
- Add a New GUI Route / Tab
1. Add a New Asset
Scenario A: User wants to track a stock (GUI operation, no code changes)
Directly use the GUI Dashboard "Add Asset" button → enter symbol → select tracking portfolio → submit. Backend POST /api/holdings → portfolio.md holdings list is automatically appended.
→ 0 code changes, any yfinance symbol works.
Scenario B: Add a completely new asset type (e.g., fund 005827.SS)
If it's on yfinance → same as Scenario A, 0 changes.
If it is not on yfinance (e.g., bank wealth management products, private equity) → follow "Add a New Data Source" (next section).
Scenario C: Add a new "quote proxy" logic
For example, gold currently uses proxy_kind: gold_cny_per_gram (GC=F + USDCNY reverse-calculated gram price). To add a new proxy (e.g., silver_cny_per_gram):
- Modify
utils/quotes.py: Add a case inget_quote()pythonif h.get("proxy_kind") == "silver_cny_per_gram": return _silver_quote(h) # Write a new reverse calculation function - Create
utils/silver_price.py: Follow the pattern ofutils/gold_price.py - Modify
core/portfolio_manager.py:HOLDING_TEMPLATES: Add a silver template - Optional: Modify the GUI HoldingDialog heuristic kind determination
Effort: ~1 hour.
2. Add a New Data Source
Example: Integrate the East Money Fund API to pull public mutual fund NAVs (not available on yfinance).
Change List
Create
services/eastmoney_fund.pypythondef fetch_fund_nav(code: str) -> Optional[float]: url = f"http://fundgz.1234567.com.cn/js/{code}.js" # ... fetch + parse ... return navModify
utils/quotes.py:get_quote()to add routingpythonif h.get("proxy_kind") == "eastmoney_fund": nav = fetch_fund_nav(h["symbol"]) return Quote(price=nav, currency="CNY", ...)Modify
connectors/web_api/routers/observability.py:get_data_sources_health()Register the new data source in the health panel (so users can see "Last fetch time for East Money Fund API")Test
tests/test_eastmoney_fund.pypythondef test_fetch_fund_nav_real_code(): nav = fetch_fund_nav("005827") assert nav > 0Update the
proxy_kindlist indocs/wiki/05-data-model.md
Effort: ~2-3 hours (including testing).
3. Add a New Agent Role
Example: Add an "ESG Analyst" to assess sustainable investment metrics.
Change List
Create
agents/esg_analyst.pypythonPROMPT_ESG_ANALYST = """ You are an ESG Analyst... Output format: ESG_SCORE: 0-100 ESG_FLAGS: [list of red flags] """ def build_esg_prompt(asset, mode="opening"): # mode: "opening" or "rebuttal" ...Modify
core/committee/debate.py:run_committeeto register in Round 1 + Round 2..N parallel looppythonesg_agent_r1 = _create_agent( build_esg_prompt(asset, "opening"), role="esg", asset=sym, round_label="opening", ) quant_r1, risk_r1, esg_r1 = _parallel_ask([ (quant_agent_r1, quant_input_r1), (risk_agent_r1, risk_input_r1), (esg_agent_r1, esg_input_r1), ])Modify the CIO prompt to synthesize the ESG transcript
Modify
connectors/web_api/routers/regime.py:get_regime_rules()+connectors/web_api/models.py:RegimeRulesResponseto register the new role in/api/regime_rulesModify GUI
routes/committee/AgentsTab.tsxto rename the "4 Roles + Rules" tab to "5 Roles"Update 02-agents.md role matrix
Synchronize Coordinator path:
skills/invest/scripts/run.sh prepare_committeemust also spawn the 5th sub-agent → This is the cost of dual paths, see 04-execution-paths.md for details (The Direct pathrun_committeereusescore/committee/, and will automatically follow after step 4 takes effect, no separate modification needed)
Effort: ~half a day (including synchronizing both paths).
4. Add a New Connector
Example: Telegram bot.
Change List
Create
connectors/telegram_bot.py- Follow the structure of
napcat_bot.py(@cmd decorator + dispatch) - Receive Telegram message → parse
/balance→ callcore/portfolio_manager.PortfolioManagerto get data - Do not modify the portfolio dict directly; must use
pm.cash_amount(...)/pm.holdings.find(...)
- Follow the structure of
systemd unit
systemd/invest-telegram.serviceini[Service] ExecStart=/path/uv run python -m connectors.telegram_botAdd
TELEGRAM_BOT_TOKENto.env.exampleWrite a README section explaining the trigger protocol
Effort: ~half a day.
Key Constraint: The connector must only perform protocol conversion; all business logic must be forwarded to core/. Violation → behavioral drift between connectors (lesson learned from early napcat modifying dicts).
5. Add a New Cron Job
Example: Send a weekly report email every Sunday.
Change List
Create
jobs/weekly_email.pypythondef run() -> Dict[str, Any]: # Fetch last 7 days verdict + PnL + data source health # Render markdown # services/notifier.send_email(...) return {"status": "success", "email_sent": True}Create
jobs/weekly_email.ymlyamlname: weekly_email description: Send weekly report every Sunday at 09:00 schedule: "0 9 * * 0" timezone: Asia/Shanghai entry: jobs.weekly_email:run enabled: trueTest
tests/test_weekly_email.pyAdd a line to
jobs/README.mdGUI
/systemCron Jobs tab displays automatically (no frontend changes needed)
Effort: ~1-2 hours.
6. Add a New Web Endpoint
Example: GET /api/portfolio/by_currency?currency=CNY returns only assets in that currency.
Change List
Add endpoint in the appropriate router under
connectors/web_api/routers/+ add Pydantic response model inconnectors/web_api/models.pypythonclass ByCurrencyResponse(BaseModel): currency: str cash: float holdings: List[HoldingV2] total: float @app.get("/api/portfolio/by_currency", response_model=ByCurrencyResponse) async def by_currency(currency: str = Query(...)): pm = _new_pm() # ... filter holdings by cost_currency ... return ByCurrencyResponse(...)Test
tests/test_web_api.pyRestart
invest-web.service(systemd does not auto hot-reload)Synchronize frontend types:
bashcd invest-gui && pnpm gen-types # Check TS compilation, add new component to use itUpdate endpoint table in 06-api.md
Effort: ~1 hour.
7. Add a New GUI Route / Tab
Scenario A: Add a completely new top-level route (e.g., /reports)
- Create
invest-gui/src/routes/Reports.tsx - Add
<Route path="reports" element={<Reports />} />ininvest-gui/src/main.tsx - Add link in the top navigation of
invest-gui/src/App.tsx - Determine if it belongs to "Core Daily" or "Secondary" (see 10-design-system.md)
Scenario B: Add a new tab in /committee
- Create
invest-gui/src/routes/committee/MyNewTab.tsx - Add an entry to the tabs array in
invest-gui/src/routes/committee/index.tsx - Note: There are already 7 tabs; consider "is an independent tab truly necessary" before adding.
Scenario C: Add a new tab in /system
Same as B, modify routes/System.tsx.
Effort: ~30 minutes (excluding specific tab content).
General Rules (Applicable to All Changes)
Things NOT to Do
- ❌ Directly modify the portfolio dict in a connector (must use
with_portfolio_tx()) - ❌ Add new fields without updating the Pydantic schema (write operations will be rejected by validation)
- ❌ Change the output format of
_render_portfolio_body_v2()(LLMs read it; breaking changes affect prompts) - ❌ Add an endpoint without updating frontend types (runtime
undefinedfield errors) - ❌ Make changes without writing tests (166 tests is the baseline; new features need at least 3 tests covering happy / error / concurrency)
Things to ALWAYS Do
- ✅ New code must update the corresponding subdirectory README (e.g.,
agents/README.md) - ✅ New concepts must update the corresponding wiki chapter (to prevent documentation drift)
- ✅ New decisions must open an ADR
- ✅ Run
uv run pytest tests/ -vmust be all green - ✅ Commit message should explain "why" not just "what was done"
Next Steps
→ 09-troubleshooting.md — Where to look when things break
→ adr/ — See how historical decisions are recorded