1. Tickers Coverage
% Ticker Items Coverage (monthly)
Price
News
Filings
For each of the last 24 months, what share of the tickers we track today do we hold data for in that month? The denominator is the same in every column on purpose, so the line reads as backfill depth: price near 70% two years ago and near 100% this year means history exists for most of the current universe but not all of it.
news_bar = tickers_with_news_that_month / total_active_tickers
filings_bar = tickers_with_filing_that_month / total_active_tickers
Coverage is reported for active and inactive tickers separately. Active means held in at least one user portfolio, so it is the only coverage a user can feel; inactive is what we carry in case somebody buys it.
2. Ingestion
Scheduled Runs
One row per scheduled job, one column per day, read as successes / failures. Split into pull (fetches raw data from EODHD and SEC EDGAR) and process (adds sentiment, summaries, embeddings and signals to what was already fetched), because a pull that keeps succeeding while processing falls behind is the failure that does not announce itself.
cell = count(status = 'succeeded') / count(status ≠ 'succeeded') from cron.job_run_details, last 14 days UTC.
Rows is how much the job actually changed, counted in its own destination table rather than taken from what the function reported about itself. Hover a day for that day's figure. A job that succeeds every run and writes nothing is flagged: that is the failure that does not raise.
Row Counts
Row counts per table for the last 24 months, so a backfill is visible rather than falling off the left edge. Counting is cheap; the sections that read item text still use a 6-month window, because carrying two years of it crashed this function once already. User tables are counted by row only — no personal data is read.
3. News Health
News per Ticker: Frequency Distribution
How many news items does each company have? Box-and-whisker shows the distribution shape: box = middle 50% (Q1–Q3), line = median, whiskers = 5th–95th percentile, dots = individual tickers beyond whiskers.
Q1 = 25th percentile, Q3 = 75th percentile, IQR = Q3 − Q1
News Sentiment: Active vs Inactive
Usable signal is the share of items carrying a direction at all: by our own definition a neutral item is data we stored and cannot act on. The donut below it is the full tone mix. The combined figure was removed: it averaged the ~15 tickers somebody actually owns into ~11k they do not, so it moved only when the universe moved.
usable_signal = 100 − neutral_share
pct = count_of_sentiment_label / total_news_items_in_group × 100
Monthly News Metrics
Positive %
Negative %
Neutral %
Redundancy %
How does tone and content duplication change month to month?
positive_pct = positive_news_that_month / total_news × 100
redundancy_pct = mean(word_overlap_score) × 100 — high means we're pulling near-duplicate stories repeatedly.
4. Filing Health
Filings per Ticker
Each bar shows how many companies have that many filings. E.g. the "4-6" bar = number of companies with 4 to 6 filings.
bar_height = count(tickers with filing_count in that bin)
Filing Types
Breakdown of SEC filing types. 8-K = current event report, 10-Q = quarterly report, 10-K = annual report.
pct = count(form_type) / total_filings
Monthly Filing Metrics
Positive %
Negative %
Neutral %
Redundancy %
Monthly tone and duplication trends for filings. Redundancy = how much content is near-copy of other filings (higher = more repetition).
positive_pct = positive_filings_that_month / total_filings × 100
redundancy_pct = mean(word_overlap_score) × 100 (0% = fully unique, 100% = identical copies)
5. Price Health
How many unusual price moves each company had, for held tickers and for the rest. An episode opens when a market-relative return exceeds twice the ticker's own trailing 60-day volatility, and closes when it reverts to noise — so one row is one move, not one unusual day.
episodes_per_ticker = count(prices_impact rows where anomaly_type = 'price_episode')
episode opens at |z| ≥ 2 where z = market_relative_return / trailing_60d_vol
The table also holds px_rsi and px_volume_x. Those are indicator readings written on two days per series only, so they are excluded here: counted in, they would add a near-constant floor to every ticker and flatten the distribution.
Glossary
| Term | What it means |
| Health Score | A 0-100 data quality score per company. Combines content quality (70% weight) and data presence (30%). Low score = that company has missing or poor-quality data. |
| Content Quality | Weighted average of four sub-metrics: uniqueness (1 − redundancy), relevancy, coverage (specificity), and confidence (corroboration). Scale: 0-1. |
| Presence | Average of three checks: is the latest price recent? are there enough news items? is there any filing in the last 180 days? 1.0 = all present, 0 = nothing. |
| Redundancy | Word-overlap (Jaccard similarity) between items about the same company. High = we're ingesting near-duplicate stories. Shown as 1 − redundancy so higher = more unique. |
| Relevancy | Whether an item is about a company we track AND carries a directional signal. Neutral-sentiment items score 0 — they're data but not a usable signal. |
| Confidence (proxy) | How sure we are about a signal. Based on corroboration: do multiple independent sources agree on the sentiment direction? Saturates at 3 sources. |
| Freshness | Days since the newest item arrived for a company. Low = up to date, high = stale data that may need re-ingestion. |
| Band (percentile) | Where a ticker's health sits in the cohort: top 10%, above median, below median, bottom 10%. It replaced sigma bands, which borrowed a guarantee the data does not give — health is an average of averages, not a bell curve — and needed a stats background to read. |
| σ (Sigma) | Standard deviation, a measure of spread. Still used for one thing here: clipping the distribution axis at mean + 3σ. |
| Rows written | How many rows a scheduled job added or updated, counted in its destination table (news.fetched_at, prices_daily.ingested_at, news_items.scored_at and so on). Measured where the data lands, never from the job's own report. |
| Usable signal | Share of news items that carry a direction: 100 − neutral share. A neutral item is something we fetched, stored and summarised but cannot act on. |
| Frequency Distribution | Box-and-whisker over "how many items does each company have?" Box = middle 50% (Q1–Q3), line = median, whiskers = P5–P95, dots = individual tickers beyond them. |
| Axis clipping | These counts are heavily right-skewed, so a handful of mega-caps stretched the axis and squeezed the box into the left edge. The axis now stops at mean + 3σ; everything past it is stacked on the right edge, counted, and still hoverable. Nothing is dropped, only the scale changes. |
| Price episode | One unusual price move, not one unusual day. Opens when a market-relative return exceeds twice the ticker's own trailing 60-day volatility (|z| ≥ 2) and closes when it reverts to noise. Written to market.prices_impact by the quant aggregator, not computed here. |
| Active / Inactive | Active = held in at least one user portfolio. Inactive = tracked but not held by anyone yet. Every split on this page uses that line, because only the active side is something a user can feel today. |
| Pull, Preprocess, Signals | The three stages of the pipeline, taken from the jobs' own names. Pull (poliviz_ingest_*) fetches raw articles, filings, prices and earnings from EODHD and SEC EDGAR. Preprocess (poliviz_process_*) adds sentiment, summaries and parsed statements to what was already fetched. Signals (poliviz_signal_*) derives scorecards, price episodes and quant signals from what was processed. Each stage can keep succeeding while the one after it falls behind, which is why they are counted separately. Housekeeping is everything else: partition maintenance and a view refresh, which produce no pipeline data. |
Product Usage
Instrumentation Health
Read this before the two blocks below it, because it says whether they mean anything. Every event should arrive carrying a session id, and every event from a signed-in user should carry a user id. When with session sits below 100%, those events join to nothing and the session figures under it are computed over a subset without saying so.
This block exists because that was the actual state of things. Before the session lifecycle migration, 2,743 of 4,114 events had no session and all 424 sessions measured zero duration, while this page reported user numbers with no indication that its input was broken. Anything older than the migration still counts against the percentage until it ages out of the 14-day window.
Anonymous visits are session ids that appear in events but have no session row: somebody browsed and never signed in. That is the denominator of landing → signup.
Sessions
One row per day: distinct sessions, distinct signed-in users, and events. A session is one visit in one tab; it rolls over after 30 minutes of inactivity, and a page refresh continues it rather than starting a new one.
duration = last_seen_at − started_at, both written server-side — the browser is not asked when it left, because the request fired while a tab closes is dropped too often to trust. A heartbeat refreshes last_seen_at while the tab is visible and in use, and analytics.close_idle_sessions() writes ended_at once a session has been quiet for 30 minutes.
Median and p90, never a mean: one tab left open all afternoon moves a mean and leaves both of these alone. Measurable counts only sessions with more than one moment of activity — a single-event visit has no duration, and folding it in as zero would walk the median toward zero as traffic grows.
What People Did
Event types by volume over the last 14 days, each with the number of distinct users behind it. The user column is the point: 400 events from one person is an enthusiast, 400 from forty is a feature. Event types are whatever the client sends, so a type that stops appearing means the call site was removed or renamed, not that the behaviour stopped.
No row content is read anywhere in this section — types, counts and timestamps only. The tables hold no IP address by design.