openvibers/openvibe.trade Commits

Trade: readable figures and one citation per series on the AI brief (#70)

OP OpenVibers committed 29 days ago · 731d69b2e5 · 4 files, +25 −7

Changes

ddiff --git a/server/domain/briefs.js b/server/domain/briefs.js
iindex 01ec8d1..df7cbb8 100644
---- a/server/domain/briefs.js
++++ b/server/domain/briefs.js
@@@ -75,7 +75,8 @@ function createBriefs({ store, config, ctx, ai, log = console }) {
content: rows.map((r) => `${r.period || iso(r.observed_at)}: ${r.value} ${r.unit}${fromSec ? ` (filing ${String(r.source_ref).split(':')[1]})` : ` (${r.source_key})`}`).join('\n'),
provenance: { service: 'trade', source_key: o.source_key },
});
-- refs.push(rows.map((r) => ({ kind: 'observation', id: r.id, at: r.observed_at })));
++ // Cited by its newest row: the one the page's observations table shows, so each citation links to a row on the page.
++ refs.push([{ kind: 'observation', id: o.id, at: o.observed_at }]);
}
for (const d of await q.docs.all(instrument.id, MAX_DOCS)) {
if (sources.length >= MAX_SOURCES) break;
ddiff --git a/server/domain/facts.js b/server/domain/facts.js
iindex 182c6ba..8cbd1e3 100644
---- a/server/domain/facts.js
++++ b/server/domain/facts.js
@@@ -47,6 +47,13 @@ const QUARTER = /^CY\d{4}Q[1-4]$/;
const YEAR = /^CY\d{4}$/;
const INSTANT = /^CY\d{4}Q[1-4]I$/;
++/** Sort key for a metric: the statement order of METRICS (quarter, then year), then anything else by name. */
++function metricOrder(metric) {
++ const [base, span] = String(metric).split('.');
++ const i = Object.keys(METRICS).indexOf(base);
++ return i < 0 ? `9:${metric}` : `${String(i).padStart(2, '0')}:${span === 'q' ? 0 : span === 'fy' ? 1 : 2}`;
++}
++
/** "Revenue (quarter)" for revenue.q; the label for anything else Trade records. */
function labelOf(metric) {
const [base, span] = String(metric).split('.');
@@@ -171,4 +178,4 @@ function createFacts({ store, ctx, sec, log = console }) {
};
}
--module.exports = { createFacts, extract, labelOf, filingUrl, METRICS, SOURCE_KEY, SOURCE_NAME };
++module.exports = { createFacts, extract, labelOf, metricOrder, filingUrl, METRICS, SOURCE_KEY, SOURCE_NAME };
ddiff --git a/server/render/views.js b/server/render/views.js
iindex 3399678..0891fbb 100644
---- a/server/render/views.js
++++ b/server/render/views.js
@@@ -1,6 +1,12 @@
'use strict';
--const { labelOf } = require('../domain/facts');
++const { labelOf, metricOrder } = require('../domain/facts');
++
++/** The stated decimal with thousands separators, digit for digit ("39544000000" → "39,544,000,000"). */
++function grouped(value) {
++ const m = /^(-?)(\d+)(\.\d+)?$/.exec(String(value));
++ return m ? `${m[1]}${m[2].replace(/\B(?=(\d{3})+(?!\d))/g, ',')}${m[3] || ''}` : String(value);
++}
/**
* Page bodies (HTML fragments for layout.renderPage). All escaping goes through
* openvibe-publishing/ssr's `html` tagged template; `raw()` only wraps HTML built here or by the
@@@ -81,7 +87,7 @@ function home({ page, recent, urls, sync, pager, latest = new Map(), briefs = []
// The exchange column only when an instrument on the page has one (a currency has none to record).
const exchanges = page.instruments.some((i) => i.exchange);
const latestCell = (o) => (o
-- ? html`<td class="num"><data value="${o.value}">${o.value}</data> ${o.unit} <span class="muted">${labelOf(o.metric)}${o.period ? `, ${o.period}` : `, ${when(o.observed_at)}`}</span>${o.freshness && o.freshness.stale ? html` ${staleBadge(o.freshness)}` : ''}</td>`
++ ? html`<td class="num"><data value="${o.value}">${grouped(o.value)}</data> ${o.unit} <span class="muted">${labelOf(o.metric)}${o.period ? `, ${o.period}` : `, ${when(o.observed_at)}`}</span>${o.freshness && o.freshness.stale ? html` ${staleBadge(o.freshness)}` : ''}</td>`
: html`<td><span class="muted">no observation yet</span></td>`);
const rows = page.instruments.map((i) => html`<tr>
<td><a href="${urls.path.instrument(i)}"><strong>${i.symbol}</strong></a></td>
@@@ -132,9 +138,10 @@ function observationsTable(instrument, observations) {
if (!observations.length) {
return html`<p class="empty" id="no-observations">No observations have been recorded for ${instrument.symbol}, so no number is shown.</p>`;
}
-- const rows = observations.map((o) => html`<tr id="${o.id}" class="${o.freshness.stale ? 'is-stale' : ''}">
++ const ordered = observations.slice().sort((a, b) => (metricOrder(a.metric) < metricOrder(b.metric) ? -1 : metricOrder(a.metric) > metricOrder(b.metric) ? 1 : 0));
++ const rows = ordered.map((o) => html`<tr id="${o.id}" class="${o.freshness.stale ? 'is-stale' : ''}">
<th scope="row">${labelOf(o.metric)}${o.period ? html` <span class="muted">(${o.period})</span>` : ''}</th>
-- <td class="num"><data value="${o.value}">${o.value}</data> ${o.unit}${o.currency && o.currency !== o.unit ? html` <span class="muted">${o.currency}</span>` : ''}</td>
++ <td class="num"><data value="${o.value}">${grouped(o.value)}</data> ${o.unit}${o.currency && o.currency !== o.unit ? html` <span class="muted">${o.currency}</span>` : ''}</td>
<td>${when(o.observed_at)}</td>
<td>${o.source.url ? html`<a href="${o.source.url}" rel="nofollow noopener">${o.source.key}</a>` : o.source.key}</td>
<td>${when(o.retrieved_at)}</td>
ddiff --git a/test/ai-briefs.test.js b/test/ai-briefs.test.js
iindex fdc3631..06e21ad 100644
---- a/test/ai-briefs.test.js
++++ b/test/ai-briefs.test.js
@@@ -116,7 +116,10 @@ const quiet = { log() {}, warn() {}, error() {} };
assert.ok(page.text.includes('AI-written, not reviewed by a person'));
const b = await t.ctx.briefs.latest(apple);
assert.ok(page.text.includes(`href="#${b.points[0].cites[0].id}"`), 'the citation links to the observation row');
-- assert.ok(page.text.includes(`id="${b.points[0].cites[0].id}"`) || page.text.includes('Revenue (quarter)'), 'the row is on the page');
++ for (const c of b.points.flatMap((p) => p.cites)) assert.ok(page.text.includes(`id="${c.id}"`), `cited row ${c.id} is on the page`);
++ assert.strictEqual(b.points[0].cites.length, 1, 'a series is cited once, by its newest row');
++ assert.ok(page.text.includes('<data value="109417000000">109,417,000,000</data> USD'), 'the stated value, digit for digit, grouped');
++ assert.ok(page.text.indexOf('Revenue (quarter)') < page.text.indexOf('Revenue (year)') && page.text.indexOf('Revenue (year)') < page.text.indexOf('Total assets'), 'statement order');
assert.ok(page.text.includes('not investment advice'));
const home = await t.get('/');
assert.ok(home.text.includes('Latest AI briefs') && home.text.includes('#ai-brief'));