Added
-
Enhanced KPI long/short conjunctive-gate screener — Phase 2a (#192).
src/orchestrators/enhanced_kpi_screener_longshort.pyships 13 of 16 issue criteria — every gate that reads an existingFundamentalsSnapshotfield plus criterion 14 via the newanalyst_recommendationfield (aliasrecommendationKey, already in the yfinanceinfopayload — no new HTTP). Paired output (enhanced-kpi-screener-longs+…-shorts); a ticker lands inlongsiff it passes ALL 14 long-side gates, inshortsiff it passes ALL 13 inverted short-side gates, otherwiseneither. Long ∩ short empty by construction. Declarative_NUMERIC_GATEStable keeps_evaluatecognitive complexity at 5. PR #228. Phase 2b owes criterion 12 (FCF margin,Ticker.cashflowfetch), criterion 15 (tech rating, blocked on #21), ADR amendments, and threshold tuning. -
Demo:
1y | 5y | 10y | alltime-window chips above the long-term F&G + 5s10s charts (#206). Click filters the entries array client-side before passing to Chart.js (per the issue's option 1 — no zoom plugin, no brush slider). New URL params?ltFgWindow=/?ycWindow=(omitted at default). Existing lazy-render guard preserved: chip click while a pane is hidden flips the rendered flag back so the next tab activation picks up the new window. PR #227. -
Demo: score-cell tooltip surfaces
mean_compositeon aggregator universes (#218). Onaggregated-scores-best/…-worst, hovering the Score cell shows the mean-of-7-composites — the metric the aggregator actually ranks on. Closes the user-confusion gap surfaced by #202 (PRSO in worst-25 with visiblescreener_score64, mean 16.5). PR #231. -
FCF margin enrichment for the long/short screener (#192 Phase 2b).
src/data_sources/fundamentals.pygains_fetch_fcf_marginmirroring_fetch_rd_to_revenue: gated oninfo["quoteType"] == "EQUITY", reads "Free Cash Flow" fromTicker.cashflowand "Total Revenue" fromTicker.income_stmtvia the shared_find_rowhelper, returnsNoneon any missing data / zero revenue / network error. Newfcf_margin: float | Nonefield onFundamentalsSnapshotattached post-validate viamodel_copy.src/orchestrators/enhanced_kpi_screener_longshort.pyadds the gate to_NUMERIC_GATES(long > 0.10, short < 0), bringing the conjunctive-gate screener to 15 of 16 criteria. Criterion 15 (tech rating) stays deferred behind #21. ADR-0002 amended to note the new enrichment field. Closes #192. -
Radar-hexagon favicon for the demo dashboard (
docs/demo/favicon.svg). A dependency-free inline SVG echoing the composite-score radar chart, theme-aware viaprefers-color-schemeso it stays legible on light and dark browser tabs. Linked fromindex.htmlastype="image/svg+xml". -
Self-hosted brand typography for the demo dashboard — Inter (UI/prose)
and JetBrains Mono (numeric table cells +<code>), per the qte77
EyeRest design tokens (#295). Latin TTF subsets ship inui/fonts/(SIL OFL 1.1,
ui/fonts/OFL.txt) withfont-display: swapand the prior system stack as
fallback — no third-party font CDN request. -
Adopted the qte77 brand mark (
logo-mark.paths.dejavu.svg) as the demo
dashboard favicon, replacing the bespoke radar SVG (#295). -
Deep history for the demo's long-term charts: CNN Fear & Greed back to
2011 and the 5s10s yield-curve slope back to 2011 (committed to the
databranch). Newscripts/backfill_yield_curve.pyforces a full
period="max"yfinance fetch — the daily cron only deepens on a first run
(#287). -
equity-spy.yamldaily cron (#288). Recomputes the SPY indexed-return
series and commits changedresults/series/equity_spy/YYYY.jsonto thedata
branch via the shared verified-commit helper. Runs 23:00 UTC, staggered 30min
after the yield-curve cron so the data-branch writers don't race the same ref.
Mirrorsyield-curve.yaml(same pinned action SHAs). -
equity_spydata source — SPY indexed-return series (#288). New
analyze_stock_kpi.data_sources.equity_spyfetches SPY (the SPDR S&P 500 ETF)
via yfinance and emits a derived rebased index (ret_indexed = close / epoch_close * 100, epoch = first close ≥ 2011) — never the raw close and never
the S&P 500 index level (ADR-0011). Same per-yearresults/series/<kind>/
shape + wrap-degrade boundary asyield_curve. Backend only here; the
data-branch backfill, the cron, and the merged-chart UI follow separately. -
Governance for the #288 equity-macro overlay (ADR-0011). Records the
decision to source the equity line from SPY (an ETF security, not the S&P
Dow Jones index) and commit only a derived indexed-return series — never
raw index levels — keeping it at the same redistribution tier as the existing
yield_curveslope. Adds adocs/data-sources.mdguardrail row and a repo
NOTICErecording the non-commercial/educational, derived-data posture, the
upstream ToS, and attribution for the bundled third-party libs (Chart.js MIT,
Fuse.js Apache-2.0). (#288) -
Forks that enable Pages now self-host their own
databranch. The dashboard
derivesDATA_BASE_URLfrom the Pages origin (<owner>.github.io/<repo>→
that owner/repo'sdatabranch) via a new pureui/lib/data.js;?base=still
overrides, and the canonical qte77 deploy is unchanged. Prior art: the sibling
agentic-job-offer-to-application-kitdashboard.
Changed
-
Aggregator derives composite-field list from
CompositeScores.model_fields.src/orchestrators/aggregated_scores_best_and_worst.pypreviously hardcoded the 7-field tuple in two places (the model definition and the orchestrator constant); adding a new composite would have been silently omitted from cross-universe ranking. Now the tuple istuple(CompositeScores.model_fields)— single source of truth, zero hidden coupling. Regression guard added intests/test_aggregated_scores_best_and_worst.py. -
Demo:
screener_scoredisplays as "qte77 Score" (#203). Display-only rename acrossKPI_GLOSSARY/ detail-panel / time-series chart / radar chart. Internal field namecomposite_scores.screener_scoreis unchanged so JSON snapshots on thedatabranch keep validating. Table column header staysScorefor column-width parity. PR #221. -
Demo: F&G rolling chart trimmed to a strict 12-month TTM window (#207).
renderFearGreedChartnow appliestrimToRollingWindow(entries, 365)before render so the rendered window is always 12 months regardless of when in the year the dashboard opens (was drifting between ~8 and ~24 months as the loader consumes this-year + last-year files). PR #222. -
Demo:
Op M %column hidden in simple view. Marks the<th>and the cellSpec asdetail-onlyso simple view keeps the 4 headline columns (Ticker / Name / Sector / Score). PR #229. -
CI: dependabot updates grouped per ecosystem.
groups: { python-deps / github-actions: { patterns: ["*"] } }on theuv+github-actionsecosystems — collapses N per-dependency PRs into one grouped PR per weekly run per ecosystem. PR #219. -
CI:
demo-snapshot.yamlfan-out readsdocs/demo/universes.jsoninstead of a hardcoded JSON literal. Adding a new universe is now a one-file change (the JSON). Empty-input dispatch re-builds every universe the dashboard knows about regardless of preset.txtstate — emitting a zero-row snapshot for an unpopulated universe is the right rebuild behavior. PR #232. -
Typed contracts in
src/orchestrators/_shared.py:AuditRowBase+DedupedSnapshot. Closes assessment gaps #2 (3 divergentAuditRowmodels) and #3 (dedup_by_tickerreturneddict[str, dict[str, Any]]).AuditRowBaseextracts the 5-field decision-trail prefix (ticker,source_universes,snapshot_dates,eligible,excluded_reason) shared by the aggregator + longshortAuditRowsubclasses; federal_contractors stays distinct (different domain — per-recipient match audit).DedupedSnapshot(BaseModel)replaces the untyped dict return ofdedup_by_ticker; consumers switch frominfo["snapshot"]string-key indexing toinfo.snapshotattribute access. Rule-of-three pre-application for Phase 2b's FCF orchestrator (#192) which becomes the 3rd consumer of both shapes. JSON shape change is safe: the aggregator + longshort audit JSONs are written but never read back (JS dashboard only consumes federal_contractors audit). Matches the AGENTS.md "every structured payload is a BaseModel" rule. -
Extracted shared snapshot-loader + paired-output writer from build scripts.
scripts/build_aggregated_scores_best_and_worst.pyandscripts/build_enhanced_kpi_screener_longshort.pywere ~80% identical (sameSOURCE_UNIVERSES, same_load_snapshots, same main-loop dict-builder, same paired-preset+audit-write pattern); CodeFactor flagged the duplicated block as its sole remaining issue onmain. Moved to a newscripts/_demo_snapshot_loader.pyexposingload_snapshots/load_all_snapshots/write_paired_universe_and_audit. Each build script collapses from ~100 LOC to ~50 LOC of orchestrator-import + thinmain(); Phase 2b's FCF orchestrator (#192) becomes a drop-in 3rd consumer.build_federal_contractors.pyleft alone (different input shape — no snapshot loading, single preset). Leading-underscore module name signals scripts/-internal per ADR-0007. -
fear-greed.yaml+yield-curve.yamlnow route throughscripts/data-branch-commit.cjs. Both daily crons carried byte-identical 36-line inlinegithub-scriptblocks (getRef → getCommit → mkBlob → createTree → createCommit → updateRef) — the same verified-commit logic thatdemo-snapshot.yaml+universe-builder.yamlalready delegate to the CJS helper. Migrating eliminates ~52 LOC of duplicated workflow YAML, ends the convention drift between the two stale crons and the two updated ones, and adds 422-race retry the inline blocks lacked (8 attempts with jittered backoff). Future verified-commit fixes propagate to all 4 workflows from a single file. Surfaced by today's scripts+workflows duplication sweep. -
README / UserStory / roadmap / architecture sync after #248. Surface the
fcf_marginenrichment field in README's sample-output blurb; mark Phase 2b shipped indocs/UserStory.mdanddocs/roadmap.md(criterion 15 / tech rating stays deferred behind #21); record the new gate count (15 long + 14 short) and the_fetch_fcf_marginboundary indocs/architecture.md. No behaviour change — pure doc drift cleanup so the long/short screener's documented state matches the merged code onmain. -
docs/demo/app.js: extractdestroyChart,scoreYAxis/themedXAxis, andtoggleHistoryHintto collapse repeated chart + empty-hint boilerplate. The chart-teardown idiom (liveCharts.delete+.destroy()) is now one helper across all six chart renderers; the 0–100 score y-axis and themed x-axis configs (each repeated 3×) become builder functions; the two byte-for-byte-identical empty-hint functions (sector-donut + long-term F&G) share onetoggleHistoryHint, while the rolling + yield-curve variants that intentionally overwrite text stay separate. Behaviour-preserving, in-file only (no new modules/tests);node --check+tscclean. First slice of the app.js size/repetition reduction. -
src/data_sources/fundamentals.py: single-source duplicated structure via_safe_ratio,_extract_two_rows,_equity_ratio. CodeFactor reported "0 duplication" but missed near-identical structure where only literals differ: the None/zero-denominator ratio guard (_compute_roi+ both EQUITY-gated fetchers), the two-row extract-with-empty/missing-row/NaN guards (_read_rd_revenue/_read_fcf_revenue, one-frame vs two-frame), and the EQUITY-gate + try/except + safe-divide fetchers (_fetch_rd_to_revenue/_fetch_fcf_margin). Behaviour-preserving — the full regression suite stays green; per-method cognitive complexity drops (both readers and both fetchers to 0) and_read_rd_revenuegains the direct unit coverage it previously lacked. Thefetchthunk in_equity_ratiokeeps the yfinance property access inside thetryand short-circuits the EQUITY gate before any fetch. -
docs/demo/lib/detail_rows.js: extract the pure detail-panel data (KPI_GLOSSARY,auditDetailRows,externalLinkRows) out ofapp.js. The KPI-glossary text and the row-tuple builders for the federal-contracts audit block + external links move to a tested lib module;app.jsimports them. Addstests/demo/detail_rows.test.mjs(7 cases for the two builders; the glossary is static data, no test). The DOM rendering (showDetail/renderRadar/renderTimeSeriesPane) intentionally stays inapp.js— extracting it would require exporting the shared mutableliveChartsSet across modules or a god-object deps argument, both worse than co-location (AHA). Behaviour-preserving. -
docs/demo/lib/format.js: extract the pure value formatters + comparator (nested,fmtNum,fmtPct,compareValues) out ofapp.jsinto a unit-tested module. Addstests/demo/format.test.mjscovering dotted-key access (incl. missing segments / nullish root),fmtNumnull/NaN/precision,fmtPctnull plus its intentional no-NaN-guard behaviour, andcompareValuesnulls-last-regardless-of-direction + string/number ordering.td()stays inapp.js(DOM glue). Behaviour-preserving; prerequisite for the upcomingtable.js/detail.jsconcern splits. -
docs/demo/lib/window.js: extract the pure time-window helpers (WINDOW_DAYS,filterByWindow,findClosestScore) out ofapp.jsinto a unit-tested module. Addstests/demo/window.test.mjscovering the non-trivial branches (empty input,"all"passthrough, unparseable-latest fallback, inclusive cutoff boundary, arbitrary iso field; closest-score nearest-match + equidistant tie-break). Also deletes the redundanttrimToRollingWindow— it was equivalent tofilterByWindow(entries, "1y", "timestamp")— and rewrites its sole call site inrenderFearGreedChart. Behaviour-preserving; the window math is now testable without a DOM. -
docs/demo/table.js: split the universe-table rendering concern out ofapp.js. MovesALL_COLUMNS, the DOM builders (renderUniverseTable,renderRow,td,annotateEmpty) and the pure helpers (buildRowTitle,coverageCount,meanComposite,totalCompositeScore,emptyTableMessage) into a new sibling module. Module state (active universe, sort key/dir, filter query, row-click handler) is now passed in via anoptsobject instead of read fromapp.jsglobals, sotable.jsholds no mutable app state.app.jskeeps a thinrenderTable()adapter, so its 7 call sites are unchanged. Addstests/demo/table.test.mjs(18 cases for the pure helpers) and widenstsconfigincludeto top-level*.js. Behaviour-preserving;app.jsdrops ~225 lines. -
docs/demo/app.js: prune verified-dead code. Removes two obsolete historical comments (the removed mobile auto-simple guard; the removed simple-mode external-link behaviour inonRowClick), the inertradarCanvas.className = "radar-canvas"assignment (no matching CSS/HTML selector — Chart.js gets the canvas node by reference), and the never-passedtextparameter ofrenderRollingEmptyHint(its sole call site always resolves toEMPTY_HISTORY). Each removal was cross-checked againstindex.html/style.css/ tests to confirm zero live references. Behaviour-preserving. -
docs/demo/detail_panel.js: extract the#row-detailside-panel lifecycle out ofapp.js.showDetail/bindDetailDismiss(plus the privatedl/closeDetailhelpers) move to a new sibling DOM module that mirrorstable.js: it owns the detail aside but takes the row's audit record and the two chart renderers (renderRadar,renderTimeSeriesPane) via a context object instead of readingapp.jsglobals, so it stays free ofapp.jsmutable state and chart slots.app.jskeeps a one-lineonRowClickadapter and drops its now-unuseddetail_rowsimport +fmtPct. Net −163 LOC inapp.js. Behaviour-preserving (logic moved verbatim); the consumed pure builders stay covered bytests/demo/detail_rows.test.mjs. -
docs/demo/lib/fetch.js: extract the data-branch fetch helpers out ofapp.js.fetchJson(fetch + throw-on-non-2xx + parse) andloadYearsFromBranch(this-year + last-year concat with per-leg silent-fail and ascending sort) move to a pure, DOM-free lib module;app.jsimports them and keeps the thin state-closing wrappers (loadManifest,loadSnapshot,loadFearGreedYears, …).loadYearsFromBranchnow takes the base URL as a parameter (loadYearsFromBranch(baseUrl, pathPrefix, sortKey)) instead of closing overDATA_BASE_URL, which is what makes it pure and testable. Addstests/demo/fetch.test.mjs(5 cases: 2xx parse, non-2xx throw, two-leg concat+sort, per-leg silent-fail, non-array-leg skip). Net −31 LOC inapp.js. Behaviour-preserving. -
docs/demo/lib/chart_axes.js: extract the themed Chart.js axis factories out ofapp.js.scoreYAxis(0–100, stepSize 25) andthemedXAxis(tick-capped x) move to a pure, DOM-free lib module. To keeplib/DOM-free they take an injectedcssVarFn(token, fallback)instead of closing overapp.js'scssVar:scoreYAxis(cssVar)/themedXAxis(cssVar)at the four call sites (renderTimeSeriesPane,renderFearGreedChart,renderMonthlyFearGreedChart,renderYieldCurveChart). The returned objects still carry deferred color closures (Chart.js resolves them lazily on theme flip), now documented as a caller-owns-lifetime contract. Addstests/demo/chart_axes.test.mjs(4 cases: static shape + deferred-injection for both factories). Net −19 LOC inapp.js. Behaviour-preserving. -
docs/demo/app.js: dedup the chart empty-hint helpers onto a singletoggleHistoryHint.renderRollingEmptyHintandrenderYieldCurveEmptyHintwere 14-line near-twins re-implementing the create/update/remove logic; both collapse to one-line delegations (matching the existingrenderDonutEmptyHint/renderLongTermEmptyHintwrappers).toggleHistoryHintgains an optionaltextparam and switches from skip-if-existing to update-if-existing — which is what preserves the "loading… → no history yet" overwrite of the static placeholders shipped in#fg-chart-wrap/#yc-chart-wrap. Verified safe for the donut + long-term callers: they have no static placeholder and only ever storeEMPTY_HISTORY, so the update path is a no-op for them. Net −24 LOC inapp.js. Behaviour-preserving. -
ui/charts.js: extract the Chart.js rendering layer out ofapp.js(closes #268). All chart builders (sector donut, radar, F&G rolling/monthly, yield curve, detail-panel time-series) plus the shared chart infra (liveCharts/cssVar/destroyChart/bindThemeObserver/toggleHistoryHint) and the long-term-tab + window-chip wiring move verbatim into a sibling module. The only app↔chart coupling is wired through a single injected context object —initCharts(ctx)with live getters/setters forsnapshot/sectorFilter/ltFgWindow/ycWindow/manifest/… andafterSectorToggle/afterWindowChangecallbacks — mirroring thetable.js/detail_panel.jsopts-object precedent. Netapp.js−722 LOC (1278 → ~615); behaviour-preserving (147 vitest pass; headless render verified: donut click-filter, tabs, window chips, detail radar + time-series all functional, 0 console errors). -
Import package renamed
src→analyze_stock_kpi(src-layout; ADR-0009, resolving the ADR-0007 refactor candidate). The package now lives atsrc/analyze_stock_kpi/and is imported asanalyze_stock_kpi(e.g.from analyze_stock_kpi.domain.universe import resolve_universe); the CLI entry ispython -m analyze_stock_kpi. Breaking for any code importingsrc.*— switch toanalyze_stock_kpi.*. The PyPI distribution name (analyze-stock-kpi), CLI behaviour, and public API are otherwise unchanged. -
Dashboard re-themed to the qte77 EyeRest brand (zero-blue, warm). The
ui/dashboard moves from its cool-gray Apple-system palette to EyeRest's umber/parchment tones (DESIGN.md). Blue/teal accents → the amber accent + the brand's zero-blue data arc across rating chips, KPI heatmap, the score-cell ramp, the sector donut, and the favicon (#278); the neutral surfaces → warm parchment (light) / umber (dark) (#282). Both light + dark, system-theme default kept, WCAG AA on the brand pairs. Fully token-driven (CSS custom properties), so scheme/variant flips re-resolve every value. -
Data-branch
results/layout grouped by kind. Per-year series moved to
results/series/{cnn_fg,yield_curve}/; every per-universe audit JSON moved
underresults/audit/<universe>/(federal-contractors, aggregated-scores,
enhanced-kpi-screener). Demo snapshots (results/demo/) and the universe
audit (results/audit/universes-*.json) are unchanged. Config, the snapshot
workflows, the demo loader, and docs all route through the new paths; the
databranch is migrated in lockstep so the live demo never 404s. Plan:
docs/plans/restructure-results.md. -
Demo header constrained to the content-column width.
headernow shares
main'smax-width: 1400px; margin: 0 auto, so on wide viewports the title and
the theme / report-issue / updated meta align with the cards instead of
stretching edge-to-edge. No effect below 1400px. -
Merged long-term-context chart (#288). The "CNN F&G long-term" and "5s10s
slope" tabs collapse into one "Long-term context" chart: the F&G monthly median
and the 5s10s monthly mean (normalized to 0-100) share the left axis, and the
SPY indexed return sits on a logarithmic right axis. All three reconcile
onto one monthly grid via the new pureui/lib/combined.js
(normalizeSlope/aggregateMonthly/buildCombinedSeries) +
logRightAxisinui/lib/chart_axes.js. SPY is the derivedequity_spyseries
on thedatabranch (ADR-0011, never raw index levels). -
NOTICEexpanded to the full third-party-attribution convention (matches the
paperverse sibling). Now opens with the project's Apache-2.0 license header
and reproduces/points to the verbatim license texts of every bundled,
redistributed component shipped in the built UI: Chart.js (MIT), Fuse.js
(Apache-2.0), and the Inter + JetBrains Mono fonts (SIL OFL 1.1) — the
fonts were previously omitted. Non-redistributed Python/JS dependencies are
explicitly excluded. Also corrects a stale "MIT" reference in
docs/data-sources.md(the repo is Apache-2.0). -
qte77 watchlist expanded to 100 symbols (+27). Added Allianz (
ALV.DE),
Deckers (DECK), Enel (ENEL.MI), Comfort Systems (FIX), Intl Container
Terminal Services (ICTEF), InterDigital (IDCC), Louisiana-Pacific (LPX),
Moody's (MCO), Meta (META), Monster Beverage (MNST), Monolithic Power
(MPWR), NetEase (NTES), Qualcomm (QCOM), REA Group (REA.AX), Sezzle
(SEZL), Sterling Infrastructure (STRL), TSMC (TSM), Clear Secure (YOU),
SK hynix (000660.KS), Zhejiang NHU (002001.SZ), kakaku.com (2371.T),
Realtek (2379.TW), MediaTek (2454.TW), Evergreen Marine (2603.TW), Yutong
Bus (600066.SS), Organo (6368.T) and Advantest (6857.T). Each symbol was
verified against the yfinance KPI surfaces the screener reads
(.info/.income_stmt/.cashflow); inline# Namecomments document the
cryptic international codes. SK hynix is the lone partial — Yahoo omits a ROIC
input so that one composite staysNone; all other KPIs populate. -
Theme toggle is now a single cycler button instead of the 3-button
segmented control. One click advances system → light → dark → system;
the button shows the active mode as an<icon> <word>label so it stays
glanceable. "System" (follow OS) remains a reachable state, and the
?theme=URL +localStoragepersistence is unchanged. A visually
hiddenaria-live="polite"status region announces each change to
screen readers, since focus stays on the button after a click. -
JS tooling + tests consolidated under
ui/with a Vite build (ADR-0010,
#289).package.json, the eslint/prettier/vitest configs, andtests/demo/
moved from the repo root intoui/(ui/tests/), so the root is Python-only.
ui/is now Vite source →ui/dist/is the deployable;gh-pages.yaml
builds (npm run build) and uploadsui/distinstead of raw-copyingui/.
Vendored Chart.js/Fuse.js,favicon.svg, anduniverses.jsonmoved to
ui/public/(served verbatim under the project base path). The dashboard's
data still loads at runtime from thedatabranch — never bundled. The
Makefile+validate.yamlJS steps now run fromui/;validatealso builds
the UI to catch breakage on PRs. -
5s10s slope chart de-noised to a weekly mean on the wide windows. The
yield-curve tab now aggregates the daily 10y−5y slope to an ISO-week mean for
the 5y / 10y / all windows (1y stays daily), mirroring the F&G long-term
monthly view so multi-year context reads as trend rather than noise. New pure
ui/lib/weekly.js(aggregateWeekly); client-side only — no change to the
data-branchresults/series/yield_curve/files. (#308) -
Refreshed the "Why these charts?" copy for the merged long-term chart
(#288). The pane described the F&G long-term and 5s10s charts as separate
tabs; it now describes the single combined chart — F&G monthly median + the
normalized 5s10s on the 0–100 axis and the SPY-derived equity index on the log
axis. -
Dashboard adopts the qte77 EyeRest brand theming, aligned with the sibling
paperverse + agentic-job-offer-to-application-kit dashboards. CSS tokens are
renamed to the brand-canonical names (--surface/--text-muted/--primary,
plus a new--primary-on) and the light/dark cascade moves tohtml[data-theme],
set by a new repo-localui/theme.jsthat mirrorsqte77.github.io/assets/theme.js:
one cycling◐/○/●button,localStorage["qte77-theme"], athemechangeevent that
recolours the charts, a dynamicaria-label+#theme-statuslive region,
prefers-reduced-motion, and a<head>anti-FOUC guard. The data arc re-tones per
theme, so rating-chip text uses--primary-onto keep contrast in both modes. The old
lib/theme.js+ its test are removed (the toggle logic now lives inui/theme.js). -
Theme toggle no longer writes
?theme=to the URL on click — it reads?theme=
on load (deep-links still work) and persists the choice tolocalStorage. -
README restructured to the qte77 doc-structure canon. Hero tagline under the H1,
then What → How → Why → References → License: the one-line pitch moves to a hero
blockquote, a new Why section states the keyless-vs-paid-feed gap, What is
trimmed to ≤7 reader-value bullets (field lists / composite formulas / preset details
now link out todocs/architecture.md), and the non-canonSample output/
Universe sourcessections fold into What/How. Orientation only; depth lives indocs/.
Fixed
-
CodeFactor E241: collapse multi-space alignment in
enhanced_kpi_screener_longshort+ its test. Three fixture dicts and a parametrize tuple table intests/test_enhanced_kpi_screener_longshort.pyplus the_NUMERIC_GATEStable insrc/orchestrators/enhanced_kpi_screener_longshort.pyused column-aligned spacing after:/,. CodeFactor's pycodestyle (which ignores the repo's ruff config) flagged ~57 E241 hits, tanking the two files to F / D grades. Collapsed to single-space; no semantic change. Preferred over a newsetup.cfg [pycodestyle]override because no sibling qte77 repo uses one andpyproject.tomlruff stays the single lint source of truth. Inline-comment, # ...sites (PEP 8 standard) untouched — CodeFactor isn't flagging those. -
Demo: dark-mode chart grid + axis labels visibility (#205). Chart.js scale defaults were theme-blind (rgba(0,0,0,0.1) grid lines, near-black tick labels) — vanished against the dark panel. Add scriptable
() => cssVar()closures forscales[].grid.color+scales[].ticks.coloracrossfearGreedChart,monthlyFearGreedChart,yieldCurveChart,timeSeriesChart,radarChart.sectorChart's static--panelslice-seam color converts to a scriptable closure too;radarChart+sectorChartregister inliveChartssobindThemeObserverre-evaluates their scriptable colors on theme flip. PR #220. -
CI:
sam.govadded to lychee's exclude list.https://sam.gov/about/terms-of-use(referenced indocs/data-sources.md) returns 403 to lychee's UA, breaking thelinkCheckerjob on every unrelated PR. Same pattern as the existingsec.gov/aaii.com/naaim.orgexclusions. PR #223. -
CI: include long/short universes in
demo-snapshot.yamlfan-out. PR #228 addedenhanced-kpi-screener-{longs,shorts}touniverse-builder.yamlanddocs/demo/universes.jsonbut misseddemo-snapshot.yaml— picker showed them, selection painted "no history yet" forever because no snapshot ever landed atresults/demo/enhanced-kpi-screener-*/…. PR #230. -
Demo: dedupe the
"no history yet"empty-hint string. Extracted into one module-levelEMPTY_HISTORYconst; replaced 5 hardcoded copies + 1 near-duplicate. AHA-warranted (5 of one literal); context-specific strings ("first cron run pending","loading manifest…") stay inline. PR #224. -
docs/demo/types.d.ts: declareRow.composite_scoresas nullable.src/data_sources/fundamentals.py:FundamentalsSnapshot.composite_scoresisCompositeScores | None, which serializes tonull. The TS declaration wascomposite_scores?: CompositeScores;— optional but not nullable; type narrowing on a literalnullvalue would have failed. Nowcomposite_scores?: CompositeScores | null;, matching the convention every other nullable field already uses (?: T | null). One-line drift fix surfaced by today's contracts audit; the remaining 11FundamentalsSnapshotfields absent fromRoware intentional per the file's "subset the dashboard reads" header policy and confirmed unused indocs/demo/. -
Sector donut legend now reappears after browser zoom-out using ResizeObserver-based auto-hide (#152).
-
docs/demo/tsconfig.json: bumpmoduleResolutionnode→bundler(andmoduleES2020→ESNext). The legacynode(node10) resolver is deprecated on the TypeScript 7.0 track and failsmake lint_js/make validatewith TS5107.bundleris the correct resolver for this browser-native ES-module demo (relative./lib/*.jsimports with explicit extensions, no Node package semantics). Dev-tooling only; no runtime or behaviour change. -
F&G backfill is now a strict gap-fill — it no longer clobbers a
CNN-direct row'ssubindicators/previous_*fields when a whit3rabbit row
shares the same midnight-UTC date key. The prior_upsert(force=False)path
replaced same-timestamp rows on any content difference; the test meant to
guard this used an unrealistic intraday timestamp and so missed it (#287). -
Demo dashboard now shows the full long-term history. The data loader only
fetched the last two years, so the CNN F&G long-term + 5s10s slope charts
stopped at ~2025 even after the data branch was backfilled to 2011 (#287).
loadYearsFromBranchnow fetches every year from the series floor (2011)
through the current year, so the 5y / 10y / all window chips paint the whole
range. -
sp500universe:BRK.B→BRK-B. Yahoo / yfinance uses a hyphen for
Berkshire Hathaway's Class B share-class symbol; the dottedBRK.Bresolves to
all-null KPI rows (verified0/3on retry-probe vsBRK-B3/3). The qte77
watchlist already usedBRK-B; this alignssp500.txt. The other empty-data
symbols from the same audit (MMC, latam.SAlines and their ADRsERJ/
EBR) are an upstream yfinancequoteSummaryissue, not symbol errors —
tracked in #312. -
No more flash-of-wrong-theme on load for an explicit light/dark
override. An inline guard in<body>resolves the theme (URL >
localStorage> system) and sets the body class before first paint,
instead of waiting for the deferredapp.jsmodule.