Skip to content

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 ​

  1. Add a New Asset
  2. Add a New Data Source
  3. Add a New Agent Role
  4. Add a New Connector (e.g., Telegram Bot)
  5. Add a New Cron Job
  6. Add a New Web Endpoint
  7. 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):

  1. Modify utils/quotes.py: Add a case in get_quote()
    python
    if h.get("proxy_kind") == "silver_cny_per_gram":
        return _silver_quote(h)  # Write a new reverse calculation function
  2. Create utils/silver_price.py: Follow the pattern of utils/gold_price.py
  3. Modify core/portfolio_manager.py:HOLDING_TEMPLATES: Add a silver template
  4. 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 ​

  1. Create services/eastmoney_fund.py

    python
    def fetch_fund_nav(code: str) -> Optional[float]:
        url = f"http://fundgz.1234567.com.cn/js/{code}.js"
        # ... fetch + parse ...
        return nav
  2. Modify utils/quotes.py:get_quote() to add routing

    python
    if h.get("proxy_kind") == "eastmoney_fund":
        nav = fetch_fund_nav(h["symbol"])
        return Quote(price=nav, currency="CNY", ...)
  3. 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")

  4. Test tests/test_eastmoney_fund.py

    python
    def test_fetch_fund_nav_real_code():
        nav = fetch_fund_nav("005827")
        assert nav > 0
  5. Update the proxy_kind list in docs/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 ​

  1. Create agents/esg_analyst.py

    python
    PROMPT_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"
        ...
  2. Modify core/committee/debate.py:run_committee to register in Round 1 + Round 2..N parallel loop

    python
    esg_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),
    ])
  3. Modify the CIO prompt to synthesize the ESG transcript

  4. Modify connectors/web_api/routers/regime.py:get_regime_rules() + connectors/web_api/models.py:RegimeRulesResponse to register the new role in /api/regime_rules

  5. Modify GUI routes/committee/AgentsTab.tsx to rename the "4 Roles + Rules" tab to "5 Roles"

  6. Update 02-agents.md role matrix

  7. Synchronize Coordinator path: skills/invest/scripts/run.sh prepare_committee must also spawn the 5th sub-agent → This is the cost of dual paths, see 04-execution-paths.md for details (The Direct path run_committee reuses core/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 ​

  1. Create connectors/telegram_bot.py

    • Follow the structure of napcat_bot.py (@cmd decorator + dispatch)
    • Receive Telegram message → parse /balance → call core/portfolio_manager.PortfolioManager to get data
    • Do not modify the portfolio dict directly; must use pm.cash_amount(...) / pm.holdings.find(...)
  2. systemd unit systemd/invest-telegram.service

    ini
    [Service]
    ExecStart=/path/uv run python -m connectors.telegram_bot
  3. Add TELEGRAM_BOT_TOKEN to .env.example

  4. Write 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 ​

  1. Create jobs/weekly_email.py

    python
    def 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}
  2. Create jobs/weekly_email.yml

    yaml
    name: weekly_email
    description: Send weekly report every Sunday at 09:00
    schedule: "0 9 * * 0"
    timezone: Asia/Shanghai
    entry: jobs.weekly_email:run
    enabled: true
  3. Test tests/test_weekly_email.py

  4. Add a line to jobs/README.md

  5. GUI /system Cron 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 ​

  1. Add endpoint in the appropriate router under connectors/web_api/routers/ + add Pydantic response model in connectors/web_api/models.py

    python
    class 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(...)
  2. Test tests/test_web_api.py

  3. Restart invest-web.service (systemd does not auto hot-reload)

  4. Synchronize frontend types:

    bash
    cd invest-gui && pnpm gen-types
    # Check TS compilation, add new component to use it
  5. Update 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) ​

  1. Create invest-gui/src/routes/Reports.tsx
  2. Add <Route path="reports" element={<Reports />} /> in invest-gui/src/main.tsx
  3. Add link in the top navigation of invest-gui/src/App.tsx
  4. Determine if it belongs to "Core Daily" or "Secondary" (see 10-design-system.md)

Scenario B: Add a new tab in /committee ​

  1. Create invest-gui/src/routes/committee/MyNewTab.tsx
  2. Add an entry to the tabs array in invest-gui/src/routes/committee/index.tsx
  3. 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 undefined field 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/ -v must 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