Files
project-work/worker-toolkit-stocks-in-the-future/repo/docs/map/objects/trading/stock.md

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; 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, through portfolio-stock; order
  • 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.

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 multiplies price_cents in SQL (app/models/portfolio.rb:48-52); portfolio-position, 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. 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