Metadata-Version: 2.4
Name: oak-domain-investments
Version: 0.8.0
Summary: Investments domain plugin for the OakQuant timber substrate (watchlist capability migrated from grove 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: 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 plugin for OakQuant's [Timber](https://pypi.org/project/timber-common/) substrate — discoverable, four-layer, and now the platform's first complete business domain.

[![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/)

## 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 app, acorn's tool registry) — never by editing the core libraries. Install it and investing appears across the platform; leave it out and nothing breaks. See the `oak-domain-plugins` skill for the wider plugin shape.

What started as the watchlist migration has grown into a complete, goal-linked investing capability: a rich watchlist, accumulation plans, a value projection, a growth scheduler that rolls into goal attainment, holdings linkage to brokerage truth, and an unlinked personal watchlist. The whole program (Phases 1 / 1.5 / 2 / 2.5 / 3) 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 group and imports only that entry point's module, so the timber layer never pulls in grove/FastAPI or acorn.

| Layer  | Entry-point group | Class                    | Contributes |
|--------|-------------------|--------------------------|-------------|
| timber | `timber.domains`  | `InvestmentsDomain`      | `WatchlistItem` + `WatchlistValueSnapshot` models and the watchlist / projection / growth / holdings-link services, registered at init (Step 8.5) into Timber's model/service registries |
| grove  | `grove.domains`   | `InvestmentsGroveDomain` | The watchlist HTTP router, mounted at `/api/v3/watchlist` on grove's app at startup |
| acorn  | `acorn.domains`   | `InvestmentsAcornDomain` | The `get_watchlist`, `get_goal_investments`, and `add_to_watchlist` agent tools, registered into acorn's Oracle at startup |
| sky    | (host registry)   | `sky-investments`        | The goal watchlist, projection modal, and home watchlist gadget 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 Timber's `service_registry.domain("investments")`), so there is one implementation behind every surface. Grove **core** additionally wires the daily growth scheduler and the allocate-to-goal endpoint, both of which consume these domain services.

## Models (`oak_domain_investments/models/`)

File-per-table, each a `common.models.base.Base` model with `TimestampMixin`, so Step 9 `create_all` creates the table.

- **`WatchlistItem`** (`watchlist_items`) — the rich, goal-linked watchlist row (migrated field-for-field from grove's live table so existing rows keep working):
  - `id` (UUID str PK), `user_id`, `symbol`, `company_name`, `sector`, `industry`, `notes`
  - `goal_id` (FK → `investment_goals.id`, **nullable** since Phase 1.5 — a NULL goal_id is an *unlinked* personal item)
  - price snapshot (`added_price`, `current_price`, `price_change_pct`, `price_updated_at`)
  - AI fields (`ai_recommendation`, `ai_confidence`, `ai_analysis`, `ai_analyzed_at`)
  - advisor fields (`advisor_recommendation`, `advisor_notes`, `advisor_reviewed_at`, `advisor_id`)
  - targets (`target_buy_price`, `target_sell_price`, `stop_loss_price`)
  - **accumulation plan** (`planned_amount`, `planned_quantity`, `accumulation_frequency` ∈ one_time/weekly/biweekly/monthly/quarterly, `accumulation_amount`)
  - `status` (default `watching`), `source`, `removed_reason`
- **`WatchlistValueSnapshot`** (`watchlist_value_snapshots`) — a per-item, per-day value point written by the growth scheduler: `watchlist_item_id`, `goal_id`, `user_id`, `snapshot_date`, `accrued_periods`, `contributed`, `shares`, value. Feeds the projection's "actual" series.

Exported as `DOMAIN_MODELS` and registered before `create_all`.

## Services (`oak_domain_investments/services/`)

Each takes its DB from the injected `ctx.db`; market enrichment uses Timber's `stock_data_service` on a best-effort basis. Per-tier limits (e.g. `max_watchlist`) are a grove-billing concern enforced by the grove router, not here.

- **`watchlist_service.py`** (`WatchlistService`) — `add_to_watchlist` (goal-linked, idempotent), `add_unlinked` (user-scoped, NULL goal_id), `get_watchlist(goal_id)`, `get_user_watchlist(user_id)` (across all goals plus unlinked), `update_watchlist_item`, `add_advisor_review`.
- **`projection.py`** — pure functions: `build_projection` (value vs contributed over a horizon, profit = the gap) and `reproject` (re-fit now from actual snapshots plus the latest model).
- **`growth.py`** (`GrowthService`) — `run_growth`: forward paper-accrual of accumulation plans, real valuation of linked positions, idempotent per item+date, and it rolls the latest per-item value into `InvestmentGoal.current_value` and `goal_progress_pct`.
- **`holdings_link.py`** (`HoldingsLinkService`) — keeps brokerage truth and goal allocation distinct: `resolve_holding_id`, `link_position` (idempotent, never re-points), `reconcile_user` (backfill), `goal_investments(goal_id)`, `allocations_for_holding(holding_id)`.
- **`info_service.py`** (`InvestmentsInfoService`) — reports migration status.

## Grove HTTP surface (`grove_plugin.py`)

`InvestmentsGroveDomain` mounts a router (re-asserting grove's `X-Grove-API-Key` boundary and goal-ownership, IDOR → 404) at **`/api/v3/watchlist`**:

- `GET    {prefix}` — the goal's watchlist
- `POST   {prefix}` — add a symbol (applies the `max_watchlist` tier gate)
- `GET    {prefix}/all` — every item the user tracks, across all goals plus unlinked
- `POST   {prefix}/unlinked` — add an unlinked personal item
- `GET    {prefix}/{item_id}/projection` — the value projection (tolerates a null goal_id)
- `PUT    {prefix}/{item_id}` — update an item (including the accumulation plan)
- `POST   {prefix}/{item_id}/advisor-review` — record an advisor review

## Acorn agent tools (`acorn_plugin.py`)

`InvestmentsAcornDomain` contributes three Oracle tools (read tools at `RiskTier.READ`, the add tool at `RiskTier.WRITE`), each a synchronous, user-scoped read/write through Timber's `db_service` (no HTTP round-trip), returning markdown:

- `get_watchlist(user_id)` — the user's watchlist
- `get_goal_investments(user_id, goal_id)` — the positions and watchlist behind a goal
- `add_to_watchlist(user_id, symbol)` — add an unlinked symbol

## How discovery works

Each host loads only its own entry-point group:

- `timber.domains` → `oak_domain_investments:InvestmentsDomain`
- `grove.domains`  → `oak_domain_investments.grove_plugin:InvestmentsGroveDomain`
- `acorn.domains`  → `oak_domain_investments.acorn_plugin:InvestmentsAcornDomain`

Once the package is installed, discovery is automatic via these entry points. 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.

## Migration status

`InvestmentsInfoService` reports what has migrated into this package. Capabilities built on the migrated watchlist (plans, projection, growth, holdings linkage) live here too. Investment logic that has **not** migrated yet still lives in timber core (`common/services/data_fetcher/`, `data_processor/portfolio_metrics.py`, `vendors/plaid_service.py`) and moves over one capability at a time.

## Install

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

Requires Python 3.11+ and `timber-common>=0.6.16`. 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
```

## License

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