convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -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`
|
||||
Reference in New Issue
Block a user