--- 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`