Metadata-Version: 2.5
Name: oak-domain-investments
Version: 0.50.0
Summary: Investments domain plugin for the OakQuant timber substrate (watchlist + plaid + data_fetcher + data_processor + analytics + forecasting + investing_operations migrated from core; more to follow).
Author-email: Pumulo Sikaneta <pumulo@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cambium-ai>=0.4.0
Requires-Dist: plaid-python>=39.2.0
Requires-Dist: timber-common>=0.6.16
Provides-Extra: dev
Requires-Dist: fastapi>=0.110; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# oak-domain-investments

**The investments domain for the OakQuant platform — a complete, goal-linked
investing product delivered as a single discoverable plugin, spanning all four
OakQuant layers with zero edits to any core library.**

[![License: Apache-2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](./LICENSE)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
[![Status: Live in production](https://img.shields.io/badge/status-live_in_production-brightgreen.svg)](#)
[![PyPI](https://img.shields.io/pypi/v/oak-domain-investments.svg)](https://pypi.org/project/oak-domain-investments/)

## What it is

Investing is a *discoverable plugin*. The package advertises itself through
entry points and registers into the host's shared registries — Timber's
model/service registries, grove's FastAPI app, grove's Celery worker + beat,
grove's config store, and acorn's Oracle tool registry — **never by editing the
core libraries**. Install it and a full investing capability appears across the
platform; leave it out and nothing breaks. See the `oak-domain-plugins` skill
for the wider plugin shape.

This is OakQuant's largest and most mature domain. It began as a single
watchlist migration and is now the whole B2C investing product: financial-goal
creation with AI vision and personalized inspiration imagery, brokerage linkage
via Plaid, portfolio and goal dashboards, a self-learning single-stock research
flow, an index×sector sentiment heatmap, a behavioral Financial DNA assessment,
an advisor review queue with SLAs, and a continually-learning prediction engine.
Grove core owns **zero** investing code or config — everything investing lives
here. The bulk of this program is **in production**.

## The four-layer shape

A single installed package plugs into four OakQuant layers, **one entry point
per group**. Each host discovers only its own entry-point group and imports only
that entry point's module, so the timber layer never pulls in grove/FastAPI or
acorn, and the acorn layer never pulls in the SQLAlchemy model stack.

| Layer  | Entry-point group | Object | Contributes |
|--------|-------------------|--------|-------------|
| timber | `timber.domains` | `oak_domain_investments:InvestmentsDomain` | ORM models (Python + bundled YAML built by the ModelFactory at Step 8.5) and ~40 registered services (watchlist, goals, portfolio, Plaid, stock/market data, forecasting, prediction brain, research, Financial DNA, advisor review, …) |
| grove (web) | `grove.domains` | `oak_domain_investments.grove_plugin:InvestmentsGroveDomain` | Ten FastAPI routers (watchlist, goals, portfolio, Plaid, advisor, prediction track-record, semantic search, stock lookup, calendar-generate) + UI options-helpers |
| grove (worker) | `grove.workers` | `oak_domain_investments.worker_plugin:InvestmentsWorkerDomain` | The adaptive-prediction Celery tasks + their beat schedule (predict / predict-universe / observe / mature) |
| grove (config) | `grove.feature_packages` | `oak_domain_investments:feature_packages_dir` | Nine bundled Tier-1 config packages (workflows, tasks, HITL screens, pages, nav, home widgets, schedulers) seeded into the grove DB |
| grove (secrets) | `grove.secret_specs` | `oak_domain_investments:secret_specs` | The market-data provider + Plaid secret specs grove fetches from ranger at boot (all optional; a missing key degrades a feature, never blocks startup) |
| acorn | `acorn.domains` | `oak_domain_investments.acorn_plugin:InvestmentsAcornDomain` | The Oracle "investments" assistant profile (financial persona) + ~20 gated agent tools |
| sky | (host widget registry) | `sky-investments` | The goal/portfolio/watchlist/heatmap/oak-tree widgets (registered into sky's widget registry; lives in the sky repo) |

Grove and acorn delegate to the **same** services the timber layer registers
(reached through `service_registry.domain("investments")`), so there is one
implementation behind every surface. The grove routers mount *outside* grove's
auth middleware and so re-assert the `X-Grove-API-Key` service boundary; caller
identity and enterprise scope arrive as canopy-forwarded `X-User-*` headers.
Every investing surface is capability-gated: only an `investments`-provisioned
tenant can reach it.

## Capabilities

### Goals & vision  *(in production)*
Config-driven financial-goal creation on the shared HITL engine: goal + timeline
intake → deterministic budget/feasibility → AI-written vision → a generated
inspiration-image gallery → a persisted `InvestmentGoal` with calendar
milestones. `GoalService` owns the lifecycle (create/update/progress/soft-delete,
assisted rewrite, in-screen budget/vision/image recalculation). The premium
**goal dashboard** adds time-adjusted progress and a summary roll-up.

### Personalized goal imagery  *(in production)*
`GoalImageService` composes grove's generic image service with cambium to
generate inspiration imagery for a goal. When a user has uploaded a self photo
(a consent-gated `PersonReference`), goal images preserve their likeness via
`gemini-2.5-flash-image`. Image regeneration is generative (token cost), so it
runs only on explicit user confirmation.

### Portfolio & holdings  *(in production)*
Manual holdings CRUD, buy/sell transactions, and brokerage→goal allocation
(`PortfolioService` + `InvestmentService`). The **portfolio dashboard** (premium)
returns holdings, per-lot positions, watchlist, and a value timeline backed by a
daily `PortfolioValueSnapshot` scheduler. Brokerage truth (a `PortfolioHolding`)
and goal allocation (a goal-linked `Position` carrying `portfolio_holding_id`)
are kept strictly distinct — allocating never mutates the holding.

### Brokerage linkage via Plaid  *(in production, Plaid PRODUCTION)*
`PlaidBridge` owns link-token creation, public-token exchange, linked-account
listing, idempotent holdings sync, and unlink — composing the configured Plaid
singleton with timber's DB and grove's generic tier guard. The
`onboard_investments` HITL flow lets a user connect a brokerage (auto-syncing
real holdings) or add a position manually, launchable from home or from chat.

### Watchlist & accumulation  *(in production)*
The rich, goal-linked `WatchlistItem` (AI + advisor recommendations, price
snapshots, buy/sell/stop targets, and an accumulation plan). Items may be
goal-linked or **unlinked** (a personal watchlist, `goal_id` nullable). The home
watchlist gadget aggregates every tracked symbol across all goals plus the
unlinked list. `GrowthService` runs a forward paper-accrual of accumulation
plans and rolls the latest per-item value into goal attainment; `projection.py`
builds the value-vs-contributed projection (profit = the gap) and can re-fit from
actual snapshots.

### Single-stock research (the oak tree)  *(in production)*
`stock_research_service` runs a config-driven, cost-disciplined flow —
explore → fundamentals → technicals → growth → news sentiment → forecast →
summarize — rendered as an interactive oak tree with category drill-down. Five of
six steps are deterministic/statistical; only the summary may touch an LLM, on a
cambium-distilled payload. The price forecast uses an ensemble whose signal
weights are **learned from realized outcomes** and records each forecast into a
feedback ledger so accuracy improves over time.

### Index & sector research (the heatmap)  *(in production)*
`SectorResearchService` explores indices across geographies/types, scores
sentiment + volatility across the ~11 GICS sectors, surfaces top companies and
movers/shakers, and renders an index×sector sentiment heatmap with per-cell
drill-down into single-stock research. Sentiment is batched-LLM per index with a
lexical fallback.

### Macro — current-events deep dive  *(built)*
`macro/` answers the question single-stock research cannot: not "how is
this company doing" but "what is happening *to* it". It reads world-scoped news
(GDELT — keyless, so it works with no vendor keys at all — plus AlphaVantage
topic news and Finnhub general news), collapses syndicated copies of one story
into one event, and buckets articles into a curated 18-theme macro taxonomy
(`data/macro/themes.yaml`): tariffs, sanctions, armed conflict,
drought and crop failure, critical-minerals shortages, political uncertainty,
central-bank policy, supply-chain disruption, the AI capital cycle, and more.

Each theme is then scored on four **named** terms rather than one opaque number —
attention (how loud, relative to the rest of the board), conviction (how
one-directional the coverage is), **market confirmation** (do the instruments
the theme says should move actually move that way, scaled by each instrument's
own volatility), and breadth (how much of the sector map it touches). That third
term is what separates this from a news summary: a theme with loud coverage and
flat proxies is reported as *loud but unpriced*, which is a different and far
more useful object than "important".

The deep dive then crosses the affected sectors against the user's **own**
holdings and watchlist, writes the desk note (one supervised model call per
theme, over facts it did not choose), and produces dated, falsifiable things to
watch — the taxonomy's own release/meeting calendar plus one-sigma trigger levels
computed from each proxy's current price.

Three refusals are built in, each of them a bug this codebase has already paid
for once. A missing market term is **redistributed, never imputed as zero** (zero
says "the market disagrees"; missing says "we could not look"). An empty board
renders its **cause**, never a blank panel that reads as a quiet week. And no
watch item ever claims an official release date — the taxonomy knows CPI is
monthly, it does not know the next print is the 11th, so every cadence item
carries a *review* date, labelled as one.

Degrades rather than blanks: with no vendor keys the board is thinner but real,
and with no model key the briefing is still written from the curated taxonomy.

### Adaptive prediction engine  *(in production)*
A shared meta-learning "brain" (`adapt/`: estimate, cascade, cluster, ledger,
backtest, track-record) that predicts short-horizon moves for a resolved
universe (watchlist ∪ default), observes realized outcomes, and matures/scores
them — closing an autonomous continual-learning loop on grove's beat. The
cascade blends multiple strategies (reversal → news-cluster → deep) over one
shared model. A premium+ **track-record** endpoint exposes the honest,
outcome-scored accuracy.

### Financial DNA  *(in production)*
`FdnaService` computes a behavioral investor profile — persona (e.g.
architect / pragmatist / guardian / explorer / delegator), risk score, key
dimensions, and deterministic allocation / strategy / guardrail recommendations —
from a 16-screen / 6-phase HITL assessment scored on both **answers and answer
timing**. Reachable as a launchable flow and via the acorn assistant.

### Advisor portfolio review  *(built, enterprise + admin)*
`AdvisorReviewService` detects holdings drift from the Financial DNA target
allocation, drafts an AI rebalancing recommendation, and routes it to the
`advisor_review` work queue with a real **SLA**; an advisor claims / reviews /
approves or rejects it. Includes SLA-monitor and drift-scan schedulers and an
admin SLA override.

### Market data & analytics  *(in production)*
A self-contained market-data stack migrated out of timber core: multi-provider
fetchers (yfinance keyless default, with Alpha Vantage / Finnhub / Polygon when
keyed); `data_processor` (returns, risk, technical indicators, portfolio
metrics); `analytics` (fundamental ratios, growth, valuation); `sentiment`
(lexical + optional LLM); `forecasting` (signal→model→ensemble); and
`stock_search` over a bundled symbol catalog that powers the type-ahead and HITL
stock pickers.

Two fetch services are registered and they are **not** interchangeable by
accident:

| registry name | service | reads through the cache? |
|---|---|---|
| `data_fetcher` | `stock_data_service` | **no** — every call hits the vendor |
| `stock_cache`  | `stock_cache_service` | **yes** — `StockDataSnapshot`, per-metric TTL (info 24h · news 1h · price 1h · financials 168h), past price ranges immutable |

`stock_cache` exposes the same four `fetch_*` methods as `data_fetcher`
(`provider_order` included), so it substitutes for it with no call-site change;
the `get_*` names remain as aliases. **The research flow (`stock_research_service`)
uses `stock_cache`.** Other callers — watchlist, alerts, growth, portfolio,
sector research, the worker's prediction loop — still hold `data_fetcher` and
refetch on every call.

### Cross-cutting integrations
The domain also contributes, via the registry, its own **calendar event
generators** (earnings / ex-dividend from holdings, goal target-dates +
milestones) and **calendar taxonomy** — `generate_goal_events` takes an optional
`goal_id=` so the goal write path can run the SAME generator for one goal and
put a new (or restored, or re-dated) goal on the calendar inside the request,
rather than waiting up to 24h for `daily_calendar_refresh`; **hydration
handlers** for the home
goals/portfolio widgets; **Rustle context-access** rules for its
context-conversations; **monitor session specs** so grove's stuck-workflow
monitor can name investing session tables generically; **usage queries** so the
billing guard can count investing tier limits (`max_goals`, `max_holdings`,
`max_watchlist`, `max_goal_workflow_runs`, `max_index_research_runs`) without
grove naming any investing model; and the **nav items + home widgets** grove
composes into its generic shell.

## Grove HTTP surface

`InvestmentsGroveDomain` mounts ten routers on grove's app (each behind the
`X-Grove-API-Key` boundary and the `investments` capability gate):

| Prefix | Purpose |
|--------|---------|
| `/api/v3/watchlist` | goal + unlinked watchlist, add/update, projection, advisor review |
| `/api/v3/goals` | goal list/detail, dashboard, assisted rewrite, in-screen recalc (budget/vision/images), reopen-save, soft-delete |
| `/api/v3/portfolio` | home snapshot, premium dashboard, manual holdings CRUD, transactions, brokerage→goal allocation, market data |
| `/api/v3/external/plaid` | link-token, token-exchange, linked accounts, holdings sync, unlink, health |
| `/api/v3/advisor` | advisor inbox, review detail, claim/approve/reject, admin SLA override |
| `/api/v3/adapt` | premium+ prediction track-record |
| `/api/v3/investments` | semantic + hybrid vector search over cached financial data |
| `/api/v3/ui` · `/api/human-tasks` | stock symbol search / by-symbol lookup (type-ahead + HITL pickers) |
| `/api/v3/calendar` | on-demand single-user calendar-event generation |

## Acorn agent surface

`InvestmentsAcornDomain` registers the **"investments" assistant profile** (the
Oracle financial persona, greeting, suggestions, and `investments`-tagged tool
resolution) plus ~20 gated tools, each user-scoped through the forwarded
principal and returning markdown. They cover the user's own data
(`get_user_goals`, `get_portfolio_holdings`, `get_goal_investments`,
`get_watchlist`, `get_financial_dna_profile`), watchlist writes
(`add_to_watchlist`), analysis (`analyze_stock_info`, `analyze_stock_news`,
`analyze_stock_price`, `analyze_portfolio`, `market_outlook`, `advice_on_topic`,
`explain_concept`, `investment_philosophy`, `quote_of_the_day`), semantic search
(`vector_search`, `search_stocks`, `search_indices`, `hybrid_search`), and
`research_stock`, which drives the grove oak-tree flow and hands back the
interactive A2UI screen. All financial knowledge lives in this package — acorn
core keeps only the generic `@tool` / grove-passthrough / profile mechanisms.

## Feature packages (bundled grove config)

The `grove.feature_packages` entry point points grove at
`feature_packages/`, ten portable, self-describing config bundles that
grove's `sync_configs` discovers and seeds into the grove DB — so the domain's
workflows/tasks/HITL screens/pages/nav travel *with the package*:

`goal_workflow` · `onboard_investments` · `financial_dna` · `stock_research` ·
`index_research` · `portfolio` · `advisor_portfolio_review` ·
`investment_growth` · `macro` · `investments` (base decisioning tables).

## Models

Two Python models (`WatchlistItem`, `WatchlistValueSnapshot`) plus ~30 tables
built from bundled YAML by the timber `ModelFactory` at Step 8.5 — byte-identical
to grove's prior definitions, so the live tables map field-for-field and
`create_all` is a no-op for them. Notable tables: `investment_goals`,
`goal_images`, `goal_milestones`, `positions`, `portfolio_holdings`,
`portfolio_transactions`, `portfolio_value_snapshots`, `plaid_item`,
`financial_account`, the read-through cache tables (`cached_stock_data`,
`cached_market_indices`, `stock_data_snapshots`, …), the workflow-session tables
(`stock_research_sessions`, `index_research_sessions`, `financial_dna_sessions`,
`create_financial_goal_sessions`, `onboard_investments_sessions`,
`advisor_review_sessions`, `macro_sessions`), and the prediction feedback tables (`predictions`,
`prediction_outcomes`, `prediction_scores`, `news_clusters`).

## How it plugs in

Each host loads only its own entry-point group; discovery is automatic once the
package is installed:

```toml
[project.entry-points."timber.domains"]
investments = "oak_domain_investments:InvestmentsDomain"

[project.entry-points."grove.domains"]
investments = "oak_domain_investments.grove_plugin:InvestmentsGroveDomain"

[project.entry-points."grove.workers"]
investments = "oak_domain_investments.worker_plugin:InvestmentsWorkerDomain"

[project.entry-points."grove.feature_packages"]
investments = "oak_domain_investments:feature_packages_dir"

[project.entry-points."grove.secret_specs"]
investments = "oak_domain_investments:secret_specs"

[project.entry-points."acorn.domains"]
investments = "oak_domain_investments.acorn_plugin:InvestmentsAcornDomain"
```

For local development against checkouts, each host also honors an env-var
fallback (e.g. `TIMBER_DOMAIN_PLUGINS=oak_domain_investments:InvestmentsDomain`)
so the plugin loads without a reinstall.

## Install

```bash
pip install oak-domain-investments
```

Requires Python 3.11+. Depends on `timber-common>=0.6.16`, `plaid-python`, and
`cambium-ai`. The import package is `oak_domain_investments`.

Develop / test against a sibling timber checkout:

```bash
# from the repo root, with ../timber on the path
PYTHONPATH=../timber:. TIMBER_DOMAIN_PLUGINS=oak_domain_investments:InvestmentsDomain \
  python3 -m pytest tests/ -q
```

## Secrets & configuration

All provider secrets are **optional** and fetched from ranger by grove's
bootstrap via the `grove.secret_specs` entry point — a missing key degrades a
feature rather than blocking startup:

- **Market data** — `ALPHA_VANTAGE_API_KEY`, `FINNHUB_API_KEY`,
  `POLYGON_API_KEY` (absent → the fetchers fall back to the keyless yfinance
  provider).
- **Plaid** — `PLAID_CLIENT_ID`, `PLAID_SECRET`, `PLAID_SECRET_PRODUCTION`,
  `PLAID_ENVIRONMENT`, `PLAID_RECOVERY` (absent → brokerage linking is simply
  unavailable).

## License

Apache-2.0. See [LICENSE](./LICENSE).
