convert pdf to md, add OVERVIEW and docs

This commit is contained in:
2026-08-17 22:30:18 +00:00
parent 95d5787868
commit 4df62d2609
44 changed files with 3307 additions and 1 deletions

View File

@@ -0,0 +1,95 @@
---
type: object
cluster: trading
universe: live
status: verified
entity: app/models/stock.rb
---
# Stock
A real, tradeable ticker with a cached price. The catalogue students buy from — curated
by admins, priced nightly by Alpha Vantage.
Verified 2026-08-16 against commit `63732df`.
## Why this shape
**Prices are cached columns, not live lookups.** `price_cents` and
`yesterday_price_cents` are plain nullable integers (`db/schema.rb:352,358`) refreshed by
[refresh-market-data](../../processes/refresh-market-data.md). Every valuation in the app
reads these columns, so the whole portfolio is priced as of the last successful job run.
No request ever calls the API.
**`price_cents` is nullable, and nothing defaults it.** A stock created by an admin
without a price has `price_cents = nil` until the nightly job runs.
`Stock#current_price` copes (`nil.to_f / 100 == 0.0`, `:19-21`), but
`Order#purchase_cost` does `stock.price_cents * shares`
(`app/models/order.rb:105-107`) and raises `NoMethodError` on `nil`. Creating a stock and
trading it the same day is the way to hit this.
**Archived means unbuyable, not untradeable.** `prevent_archived_stock_purchase` is
guarded by `if: -> { buy? }` (`app/models/order.rb:25,189-193`), so students can still
**sell** an archived holding — deliberate, since archiving must not trap anyone's money.
Deletion is blocked outright: both associations are `dependent: :restrict_with_error`
(`:4-5`). Archive is the only retirement path.
**Two writers disagree about the analyst columns.** Admins may set all twenty-odd fields
(`app/controllers/admin/stocks_controller.rb:80-104`), but the weekly
`StockAttributeUpdate` overwrites only six — `company_name`, `description`,
`stock_exchange`, `industry`, `company_website`, `profit_margin`
(`app/services/stock_attribute_update.rb:62-72`). Hand-edit one of those six and the
Saturday job will silently revert it. The rest (`debt`, `cash_flow`, `debt_to_equity`,
`sales_growth`, `employees`, `management`, `competitor_names`, the three `industry_avg_*`)
are admin-only and never auto-updated.
## Shape
- Table `stocks`, `db/schema.rb:335-360`; `ticker` uniquely indexed (`db/schema.rb:359`)
- `validates :ticker, presence: true` (`:7`) — the column itself is nullable
- `company_website` must be a valid http/https URL, blank allowed (`:8-14`)
- `archived` boolean, default false, `null: false` (`db/schema.rb:336`)
- `last_trading_day` date — the freshness gate the price job compares against
- Scopes `active` / `archived` (`:16-17`)
- Readers in **dollars**: `current_price` (`:19`), `yesterday_price` (`:23`),
`percentage_change` (`:29`), `percentage_change_formatted` (`:35`)
- `yesterday_price` falls back to `current_price` when null, so day-one change is 0%
(`:23-27,29-33`)
## Connected to
- **owns:** —
- **owned-by:** —
- **joins:** [portfolio](../money/portfolio.md), through
[portfolio-stock](portfolio-stock.md); [order](order.md)
- **looks-like-but-is-not:** `price_cents` is the *cached* price, not a market price at
order time. An order placed at 9am executes at whatever `price_cents` says when the job
runs — see [place-and-execute-order](../../processes/place-and-execute-order.md).
## If you change this
- **Hits:** [order](order.md) — `purchase_cost`, all funds validations, and four sorting
scopes join this table (`app/models/order.rb:54-73`);
[portfolio](../money/portfolio.md)`#holdings_value_cents`, which multiplies
`price_cents` in SQL (`app/models/portfolio.rb:48-52`);
[portfolio-position](portfolio-position.md), whose gain/loss maths is raw SQL over
`stocks.price_cents` (`app/models/portfolio_position.rb:30-38`);
`ApplicationController#set_navbar_stocks`, which loads active stocks on **every**
request (`app/controllers/application_controller.rb:22-24`).
- **Does not hit:** [portfolio-transaction](../money/portfolio-transaction.md). Ledger
rows store the cents paid at execution time and never re-read the stock — a price
change never rewrites history, it only re-values current holdings.
## Surfaces
| Surface | Role |
|---|---|
| `Admin::StocksController` | admin CRUD (all columns) |
| `StocksController` (`index`, `show`) | student/teacher read |
| `StockPricesUpdateJob` | writes prices nightly |
| `StockAttributeUpdateJob` | writes six attributes weekly |
| every layout, via `@navbar_stocks` | reads |
## See
- Source: `app/models/stock.rb`, `db/schema.rb:335-360`