4.4 KiB
type, cluster, universe, status, entity
| type | cluster | universe | status | entity |
|---|---|---|---|---|
| object | trading | live | verified | 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. 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;tickeruniquely indexed (db/schema.rb:359) validates :ticker, presence: true(:7) — the column itself is nullablecompany_websitemust be a valid http/https URL, blank allowed (:8-14)archivedboolean, default false,null: false(db/schema.rb:336)last_trading_daydate — 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_pricefalls back tocurrent_pricewhen null, so day-one change is 0% (:23-27,29-33)
Connected to
- owns: —
- owned-by: —
- joins: portfolio, through portfolio-stock; order
- looks-like-but-is-not:
price_centsis the cached price, not a market price at order time. An order placed at 9am executes at whateverprice_centssays when the job runs — see place-and-execute-order.
If you change this
- Hits: order —
purchase_cost, all funds validations, and four sorting scopes join this table (app/models/order.rb:54-73); portfolio#holdings_value_cents, which multipliesprice_centsin SQL (app/models/portfolio.rb:48-52); portfolio-position, whose gain/loss maths is raw SQL overstocks.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. 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