ui: aggregated read on top, hide stale rows, wire /log tone toggle; prompts v8

- dashboard grid: explicit "header" area as the first row so the
    aggregated read panel renders at the top instead of being
    auto-placed after the named areas.
  - indicators: hide rows flagged stale (older than the group's
    freshness threshold). Server still computes stale_symbols;
    rendering can be re-enabled by removing the
    `{% if not is_stale %}` wrapper in indicators.html.
  - /log: add tone-changed to #log-content's hx-trigger and include
    it in cassandraSetTone's selector list — toggling Novice /
    Intermediate on the Log page was previously a no-op.
  - prompts: bump PROMPT_VERSION 7→8. Strengthen the rational-vs-
    irrational framing in the strategic-log system prompt from
    aspirational to mandatory ("a paragraph without both lenses must
    be rewritten"). Require the same lens in the per-group summary,
    cross-asset aggregate, and portfolio commentary overrides.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-23 19:36:04 +02:00
parent f326b41a08
commit b98d8d003c
6 changed files with 60 additions and 16 deletions

View file

@ -30,7 +30,7 @@ OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
# v7 (2026-05-18): Forbid "(Updated HH:MM UTC)" clauses in the date header —
# the model was hallucinating future times. The user prompt now carries the
# actual current UTC time so the model has accurate temporal context.
PROMPT_VERSION = 7
PROMPT_VERSION = 8
# --- Core: invariant across tone/analysis settings ----------------------------
@ -85,16 +85,25 @@ omit the paragraph.
"things to watch tomorrow". Each watch item should name a level/threshold \
whose breach would change the regime, not a calendar-date event.
# Rational vs irrational framing
# Rational vs irrational framing (MANDATORY in every paragraph)
The reader's primary goal is to disconnect rational decisions from market \
irrationality. In every sector or theme paragraph, separately identify:
- The RATIONAL drivers: earnings, real-economy data, monetary policy, \
structural geopolitical shifts, valuation vs fundamentals.
- The IRRATIONAL drivers: positioning, narrative momentum, sentiment \
extremes, concentration, flow-driven moves, options gamma, credit complacency.
When the two diverge price moving on irrational drivers while fundamentals \
say otherwise, or vice versa flag the divergence explicitly. Those gaps \
are where the next regime change starts.
irrationality. This is the single most important lens of the log it MUST \
appear in every sector or theme paragraph, not just where it feels natural. \
For each paragraph, before writing it, ask yourself the two questions and \
then make both answers visible in the prose:
- The RATIONAL drivers what the underlying factors justify: earnings, \
real-economy data, monetary policy, structural geopolitical shifts, \
valuation vs fundamentals.
- The IRRATIONAL drivers what the crowd is doing regardless of fundamentals: \
positioning, narrative momentum, sentiment extremes, concentration, \
flow-driven moves, options gamma, credit complacency.
Then state the GAP: is price moving with the rational read, ahead of it, \
or against it? If they agree, say so briefly and move on. If they diverge \
price moving on irrational drivers while fundamentals say otherwise, or \
vice versa name the divergence explicitly. Those gaps are where the next \
regime change starts and are the whole point of this log.
A paragraph that names only price action or only fundamentals, without \
both lenses, is incomplete and must be rewritten.
# Discipline
- No emojis, no marketing language, no "concerning" or "unprecedented" \
@ -302,6 +311,13 @@ They can see the values. They CANNOT see the meaning. Your job is to \
a regime-level interpretation, a fundamental driver identification, or a \
cross-indicator implication not a description of moves.
# Rational vs irrational lens (required at this length too)
Even at 2-3 sentences, contrast what the underlying factors justify \
(rational: fundamentals, policy, valuation) with what the crowd is doing \
(irrational: positioning, narrative, flows) whenever the two diverge. If \
they don't diverge, say so in one clause. Never just describe the move \
without placing it on this axis.
# Hard constraints
- Plain prose, ONE paragraph. No markdown, no headers, no lists, no labels.
- Open IMMEDIATELY with substance. NEVER start with: "I need to", "I'll", \
@ -350,6 +366,13 @@ Your job is NOT to summarise the moves. It is to explain what the moves, \
which divergences are load-bearing, what fundamental story the cross-asset \
behaviour tells.
# Rational vs irrational lens (required at this length too)
The cross-asset tape's value is in the gap between what the underlying \
factors justify (rational: fundamentals, policy, valuation) and what the \
crowd is actually doing (irrational: positioning, narrative momentum, \
flows). At least one of the 2-4 sentences must name this gap or, if the \
two cohere, explicitly say so.
# Hard constraints
- Plain prose, ONE paragraph. No markdown, headers, lists, or labels.
- Open IMMEDIATELY with substance. NEVER start with: "I need to", "I'll", \

View file

@ -250,6 +250,19 @@ implies X under scenario Y"), not advice ("buy X" / "sell Y" are forbidden).
what would invalidate the current posture.
- ~350 words. No bullet lists. No buy/sell recommendations.
- Do not repeat the input data verbatim interpret it.
# Rational vs irrational lens (mandatory)
Carry the base prompt's rational-vs-irrational framing through to every
paragraph of the portfolio read. For each section above, contrast:
- The RATIONAL read: what the underlying factors (fundamentals,
macro/policy regime, valuation, currency dynamics) justify for this
exposure;
- The IRRATIONAL read: what positioning, narrative momentum, sentiment
or flows are doing to that same exposure right now.
Then name the GAP does the holder's posture line up with the rational
read, or is it riding the irrational one? A paragraph that names only
the pie's numbers or only the macro backdrop, without placing the
holding on this rational-vs-irrational axis, is incomplete.
"""

View file

@ -140,8 +140,9 @@ a:hover { text-decoration: underline; }
padding: 14px;
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
grid-template-rows: auto auto auto;
grid-template-rows: auto auto auto auto;
grid-template-areas:
"header header"
"indicators log"
"portfolio log"
"news news";
@ -150,10 +151,11 @@ a:hover { text-decoration: underline; }
@media (max-width: 1100px) {
.app-main {
grid-template-columns: 1fr;
grid-template-areas: "indicators" "portfolio" "log" "news";
grid-template-areas: "header" "indicators" "portfolio" "log" "news";
}
}
#dash-header-container { grid-area: header; }
#indicators-panel { grid-area: indicators; }
#portfolio-panel { grid-area: portfolio; }
#log-panel {

View file

@ -106,7 +106,7 @@
// listen to. Simpler still: fire htmx.trigger on the well-known
// panels. We use the simple path.
['#dash-header-container', '#log-panel .panel-body',
'#indicators-body'].forEach(function (sel) {
'#indicators-body', '#log-content'].forEach(function (sel) {
var el = document.querySelector(sel);
if (el && window.htmx) window.htmx.trigger(el, 'tone-changed');
});

View file

@ -25,7 +25,7 @@
<article id="log-content"
class="log-page__content"
hx-get="/api/log/by-date/{{ selected_iso }}?as=html"
hx-trigger="load"
hx-trigger="load, tone-changed"
hx-swap="innerHTML">
<div class="empty">loading log…</div>
</article>

View file

@ -30,7 +30,12 @@
<tbody>
{% for q in quotes %}
{% set is_stale = stale_symbols and q.symbol in stale_symbols %}
<tr class="{% if is_stale %}row-stale{% endif %}">
{# Stale rows (last observation older than the group's freshness
threshold) are hidden from the default UI but the server still
computes them — remove the `{% if not is_stale %}` guard to
resurface the dimmed row + stale tag. #}
{% if not is_stale %}
<tr>
{% set tip = notes.get(q.symbol, '') if notes else '' %}
{# Long Eurostat ('dataset?...') and ONS ('topic/.../cdid/dataset') symbols
get truncated for display; hover shows the full identifier via title.
@ -39,7 +44,7 @@
{% if '?' in short_sym %}{% set short_sym = short_sym.split('?')[0] %}{% endif %}
{% if '/' in short_sym %}{% set short_sym = short_sym.split('/')[-2] | upper %}{% endif %}
<td class="label has-tip" title="{{ q.symbol }}{% if tip %} — {{ tip }}{% endif %}">
{{ short_sym }}{% if is_stale %} <span class="stale-tag" title="last observation older than 90 days">stale</span>{% endif %}
{{ short_sym }}
</td>
<td {% if tip %}title="{{ tip }}"{% endif %}>{{ q.label or "" }}</td>
<td class="num">{{ q.price | price }}</td>
@ -58,6 +63,7 @@
{% endif %}
<td class="neu">{{ q.as_of or "" }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>