From b98d8d003cb445facd90973827a609933d9d91b2 Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Sat, 23 May 2026 19:36:04 +0200 Subject: [PATCH] ui: aggregated read on top, hide stale rows, wire /log tone toggle; prompts v8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/services/openrouter.py | 43 ++++++++++++++++++++------ app/services/portfolio_analysis.py | 13 ++++++++ app/static/css/cassandra.css | 6 ++-- app/templates/base.html | 2 +- app/templates/log.html | 2 +- app/templates/partials/indicators.html | 10 ++++-- 6 files changed, 60 insertions(+), 16 deletions(-) diff --git a/app/services/openrouter.py b/app/services/openrouter.py index b3bc2c7..f1402f4 100644 --- a/app/services/openrouter.py +++ b/app/services/openrouter.py @@ -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", \ diff --git a/app/services/portfolio_analysis.py b/app/services/portfolio_analysis.py index 9d2ec7f..eb8a349 100644 --- a/app/services/portfolio_analysis.py +++ b/app/services/portfolio_analysis.py @@ -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. """ diff --git a/app/static/css/cassandra.css b/app/static/css/cassandra.css index e1e04fc..9bb7ffc 100644 --- a/app/static/css/cassandra.css +++ b/app/static/css/cassandra.css @@ -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 { diff --git a/app/templates/base.html b/app/templates/base.html index 29a2425..9f08c7e 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -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'); }); diff --git a/app/templates/log.html b/app/templates/log.html index 34c37f9..3e56727 100644 --- a/app/templates/log.html +++ b/app/templates/log.html @@ -25,7 +25,7 @@
loading log…
diff --git a/app/templates/partials/indicators.html b/app/templates/partials/indicators.html index 3639e8d..0ae1e1f 100644 --- a/app/templates/partials/indicators.html +++ b/app/templates/partials/indicators.html @@ -30,7 +30,12 @@ {% for q in quotes %} {% set is_stale = stale_symbols and q.symbol in stale_symbols %} - + {# 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 %} + {% 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 %} - {{ short_sym }}{% if is_stale %} stale{% endif %} + {{ short_sym }} {{ q.label or "" }} {{ q.price | price }} @@ -58,6 +63,7 @@ {% endif %} {{ q.as_of or "" }} + {% endif %} {% endfor %}