compliance: flag-gate AI portfolio + cloud sync + Stripe; de-risk prompts; harden reviewer
Implements docs/read-markets-compliance-changes.md as flag-gated changes (no deletions) so paused features stay in the tree for future re-enable. All four flags default False so a fresh deploy is compliance-safe. - New env flags: PORTFOLIO_AI_ENABLED, PORTFOLIO_SYNC_ENABLED, TICKER_UNIVERSE_AGGREGATE_ENABLED, SUBSCRIPTIONS_ENABLED. - Gates: /api/analyze, /api/portfolio/sync*, /api/stripe/*, /pricing, ticker_universe writes, portfolio_analysis.analyse(). is_paid_active() returns True for any auth'd user when subscriptions are paused. - Prompts (PROMPT_VERSION 10): universal _COMPLIANCE_RIDER prepended to every system prompt; watch list removed; price-target / close-above-below / trigger / forward-state-as-description rules added; SPECULATIVE pivoted to regime-only scenarios; daily + weekly digests tightened. - Reviewer: deterministic regex/lexicon pre-check fail-closed under the Haiku call; portfolio rider gated by PORTFOLIO_AI_ENABLED; base prompt sharpened for forward-state and MAR forward-opinion patterns; ReviewerVerdict audit table; generate_with_review retry helper. - Migration 0026: purge portfolio_sync + ticker_universe; create reviewer_verdicts. - Copy: MAR cite fixed to Art 3(1)(35) + Art 20 + Del Reg 2016/958; portfolio reframed as browser-only viewer in disclaimer / privacy / terms / about / pricing / landing (en + it). TODO(legal) marker for lawyer sign-off on disclaimer. - Tests: 13 lexicon + 6 reviewer compliance regressions; conftest enables all flags so existing 402 tests still cover their code paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ee8384f1ba
commit
47dce1a1a4
38 changed files with 1188 additions and 279 deletions
|
|
@ -28,7 +28,47 @@ from datetime import datetime
|
|||
# the model was hallucinating future times. The user prompt now carries the
|
||||
# actual current UTC time so the model has accurate temporal context.
|
||||
# v9 (2026-05-25): Adds daily + weekly digest prompt builders for email.
|
||||
PROMPT_VERSION = 9
|
||||
# v10 (2026-05-29): Compliance pass. Drops the watch-list section, removes
|
||||
# price-level / "close above/below" / floor-ceiling / tripwire framing across
|
||||
# log + indicator reads + chat + digests, adds a universal _COMPLIANCE_RIDER
|
||||
# prepended to every system prompt. See docs/read-markets-compliance-changes.md.
|
||||
PROMPT_VERSION = 10
|
||||
|
||||
|
||||
# --- Universal compliance rider ----------------------------------------------
|
||||
|
||||
# Prepended to every system prompt below (log, per-group indicator read,
|
||||
# aggregate read, chat, daily + weekly digests). The inline edits in _CORE,
|
||||
# _CHAT_OVERRIDES, and the summary prompt builders remove the worst patterns
|
||||
# directly; this rider is belt-and-braces so a future tone/style tweak can't
|
||||
# silently regress past it. See docs/read-markets-compliance-changes.md TASK 3.
|
||||
_COMPLIANCE_RIDER = """# Editorial perimeter (overrides everything below)
|
||||
You are an editorial market explainer, not an adviser or forecaster.
|
||||
- Explain what moved and WHY (fundamentals, policy, valuation, positioning).
|
||||
- Separate rational drivers from irrational/positioning drivers.
|
||||
- DO NOT predict future prices or give price targets, levels, floors, ceilings,
|
||||
or "close above/below X" triggers for any instrument.
|
||||
- DO NOT recommend or imply any action: no buy/sell/hold, no add/trim/rebalance,
|
||||
no overweight/underweight, no "you should", no "watch for X to do Y".
|
||||
- DO NOT use technical-analysis or chart-pattern framing
|
||||
(head-and-shoulders, support/resistance, breakouts, RSI, Fibonacci, etc.).
|
||||
- Refer to instruments to explain the present, never to advise on the future.
|
||||
- Forward-looking opinions on a named instrument's price or value are out of
|
||||
scope even without a numeric target. "Brent is likely to consolidate near
|
||||
$90–93" or "the base case is a move to X" both cross the line.
|
||||
Write as commentary on public data for a general audience.
|
||||
|
||||
Positive calibration example:
|
||||
- AVOID: "Gold $4,600 — a close above would confirm the safe-haven bid is
|
||||
returning; failure at that level would mean the peace script is dominant."
|
||||
- PREFER: "Gold is rising alongside equities while oil slides — a decoupling
|
||||
from the usual 'peace = gold weak' narrative. The most natural read is
|
||||
that safe-haven/hedging demand is re-emerging even as risk assets hold up;
|
||||
that tension is the day's most notable signal about how fully the peace
|
||||
scenario is actually priced."
|
||||
The 'prefer' version keeps the analytical insight; it drops the price level,
|
||||
the confirm/fail conditional, and the implied action.
|
||||
"""
|
||||
|
||||
|
||||
# --- Core: invariant across tone/analysis settings ----------------------------
|
||||
|
|
@ -69,8 +109,10 @@ weather or generic context.
|
|||
- Then 4-6 paragraphs, each anchored on a sleeve, sector, or theme. Concrete \
|
||||
numbers in every paragraph. No section over ~150 words.
|
||||
- One paragraph synthesising the news flow into a market read.
|
||||
- End with a watch list: 3-5 specific items to track in the next week, \
|
||||
each one sentence.
|
||||
- Close with the synthesis paragraph (and the System temperature line below). \
|
||||
Do NOT add a "watch list", "what to monitor", "tripwires", or any equivalent \
|
||||
section. A list of conditional price predictions is a forecast framework — \
|
||||
which this log is not.
|
||||
|
||||
# Time-horizon discipline
|
||||
- This is a STRATEGIC log, not a day-trader's read. Treat 1-day moves under \
|
||||
|
|
@ -79,9 +121,6 @@ multi-week trend or are extreme outliers.
|
|||
- Anchor every claim to multi-week (1m), multi-month (since-anchor), or \
|
||||
multi-year (1y) changes — not 1d. If the only thing happening is a 1d move, \
|
||||
omit the paragraph.
|
||||
- The watch list is for "structural tripwires over the next 1-3 months", not \
|
||||
"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 (MANDATORY in every paragraph)
|
||||
The reader's primary goal is to disconnect rational decisions from market \
|
||||
|
|
@ -110,8 +149,23 @@ without a specific number behind it.
|
|||
- Distinguish "the thesis predicted X and X happened" from "the thesis \
|
||||
predicted X and X did not happen". Both are useful; conflating them is not.
|
||||
- Don't repeat the same point in different words across paragraphs.
|
||||
- No buy/sell recommendations. Triggers are pre-set elsewhere; your job is \
|
||||
to report whether reality is confirming, modifying, or refuting the thesis.
|
||||
- No buy/sell recommendations. No add/trim/rebalance, no overweight/underweight, \
|
||||
no "you should", no "investors should", no "we recommend".
|
||||
- No price targets. No "close above/below $X" or "break above/below X" framing. \
|
||||
No floors, ceilings, support, resistance, or any level cast as a trigger or \
|
||||
tripwire whose breach would mean something. Citing where a price is right \
|
||||
now is fine ("Brent at $90"); casting a price as a threshold ("$93 is the \
|
||||
ceiling") is not.
|
||||
- No forward conditional calls on a named instrument's price. "Would confirm \
|
||||
X if Y", "base case is consolidation near $X", "watch for Brent to break Y" \
|
||||
are all out. Forward language belongs to the *regime* and *fundamentals* — \
|
||||
"the policy mix is tightening", "real yields stay restrictive", "positioning \
|
||||
is crowded" — not to a number on a chart.
|
||||
- Forward predictions wrapped in present-tense state language are still \
|
||||
predictions. "Valuations are stretched and unlikely to hold", "the path of \
|
||||
least resistance is lower", "risk is skewed to the downside" all smuggle a \
|
||||
forecast into description. State the state ("valuations are stretched"); \
|
||||
don't tack a direction onto it.
|
||||
|
||||
# Stance (educational, anti-TA, anti-gambling)
|
||||
The target reader is most likely young, new to investing, and at risk of \
|
||||
|
|
@ -119,9 +173,10 @@ treating markets like a horse race they need to "read" via chart patterns. \
|
|||
Cassandra is the corrective.
|
||||
- **No technical analysis.** Head-and-shoulders, RSI thresholds, Fibonacci \
|
||||
levels, Elliott waves, "support/resistance" — these are descriptions of past \
|
||||
crowd behaviour, not predictions. Don't use them; don't legitimise them. If \
|
||||
you mention a price level, frame it as a positioning fact (e.g. "the level \
|
||||
where the latest tranche of buyers entered"), not a signal.
|
||||
crowd behaviour, not predictions. Don't use them; don't legitimise them. \
|
||||
Don't cast specific price levels as load-bearing for the read; \
|
||||
spot prices and percent changes are fine ("Brent at $90", "+12% YTD") but \
|
||||
"$93 is the level to watch" is not.
|
||||
- **No gambling framing.** Markets are not a coin flip and not a horse race. \
|
||||
Never present a position as a single decisive moment, a "now or never", or a \
|
||||
bet to be won. Every read should follow the shape: *regime → implication → \
|
||||
|
|
@ -135,9 +190,9 @@ Close the log with a single sentence on a line of its own, formatted exactly:
|
|||
|
||||
System temperature: [cool|neutral|elevated|hot|extreme] — [one clause naming the 2-3 specific divergences or readings that justify the label]
|
||||
|
||||
This is the line a reader who only sees the watch list scrolls down to. Make \
|
||||
it earn its place: cite real signals (HY OAS, breadth, VIX, valuation, real \
|
||||
yields), not vibes.
|
||||
This is the line a glancing reader scrolls to first. Make it earn its place: \
|
||||
cite real signals (HY OAS, breadth, VIX, valuation, real yields), not vibes. \
|
||||
The label is a description of the current regime, not a forecast.
|
||||
|
||||
# Update mode (when an earlier log from today is provided)
|
||||
If the user message includes a section labelled "Earlier log from today \
|
||||
|
|
@ -148,8 +203,6 @@ that timestamp: confirmations, refutations, new emergent patterns.
|
|||
- The TL;DR should lead with the move since the earlier read when there \
|
||||
was a meaningful intra-day change ("Since this morning's read, …") — \
|
||||
otherwise stay regime-level.
|
||||
- The watch list should evolve: drop items that triggered or settled, add \
|
||||
items that emerged. Keep items still load-bearing.
|
||||
- Preserve any insights from the earlier draft that remain valid; sharpen \
|
||||
or revise the ones that don't. Avoid contradicting yourself silently — if \
|
||||
you change a stance, name it briefly ("Earlier I read X; with Y now, the \
|
||||
|
|
@ -250,17 +303,18 @@ def _resolve_tone(tone: str) -> str:
|
|||
_ANALYSIS: dict[str, str] = {
|
||||
"DRY": """# Analysis style: dry
|
||||
Report what happened. Identify divergences and contradictions. Compare to \
|
||||
references. Do not speculate on what comes next. Forward-looking statements \
|
||||
are limited to "what would invalidate the read" — never "we expect X to \
|
||||
happen". The watch list contains items to monitor, not predictions.""",
|
||||
references. Do not speculate on what comes next.""",
|
||||
|
||||
"SPECULATIVE": """# Analysis style: speculative
|
||||
Report what happened, then explicitly explore forward scenarios. For each \
|
||||
significant sector or theme, sketch a 1-4 week scenario set: the base case \
|
||||
(what the data suggests), a contrarian case (what would invalidate it), and \
|
||||
what tape signal would tip you from one to the other. Be explicit about \
|
||||
uncertainty — say "the base case is" not "X will happen". The watch list is \
|
||||
the trip-wires that decide between scenarios.""",
|
||||
Report what happened, then explore forward *regimes* — never forward prices. \
|
||||
For each significant sector or theme, you may sketch what the underlying \
|
||||
fundamentals and positioning suggest about the prevailing macro regime \
|
||||
(e.g. "the policy mix is still tightening", "real yields remain restrictive", \
|
||||
"crowded positioning leaves little fuel for further upside in this style"). \
|
||||
What you must NOT do is forecast the price or value of any specific named \
|
||||
instrument, even hedged with "base case is X" or "likely to consolidate near \
|
||||
$X" — those are MAR investment recommendations and outside scope. \
|
||||
Stay at the regime / fundamentals level.""",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -268,7 +322,7 @@ def build_system_prompt(tone: str, analysis: str) -> str:
|
|||
"""Compose the system prompt from the chosen audience and analysis style."""
|
||||
tone_block = _TONE[_resolve_tone(tone)]
|
||||
analysis_block = _ANALYSIS.get(analysis.upper(), _ANALYSIS["SPECULATIVE"])
|
||||
return "\n\n".join([_CORE, tone_block, analysis_block])
|
||||
return "\n\n".join([_COMPLIANCE_RIDER, _CORE, tone_block, analysis_block])
|
||||
|
||||
|
||||
# Backwards-compat: a default-composed SYSTEM_PROMPT for tests / callers that
|
||||
|
|
@ -281,7 +335,7 @@ SYSTEM_PROMPT = build_system_prompt("INTERMEDIATE", "SPECULATIVE")
|
|||
_CHAT_OVERRIDES = """# Chat mode (overrides the log-structure rules above)
|
||||
You are NOT writing a daily log right now. The user is asking a specific
|
||||
question via the chat sidebar.
|
||||
- Forget the date header, TL;DR, sectional structure, and watch list. Just answer.
|
||||
- Forget the date header, TL;DR, and sectional structure. Just answer.
|
||||
- Typical response: 200-400 words. Longer only if the question genuinely
|
||||
warrants it.
|
||||
- Cite specific numbers and named headlines from the reference materials
|
||||
|
|
@ -289,7 +343,15 @@ question via the chat sidebar.
|
|||
- If a question is outside the provided context (e.g. asking about a stock or
|
||||
event not in the data), say so plainly rather than speculating from prior
|
||||
knowledge.
|
||||
- No buy/sell recommendations. If asked, redirect to thesis and scenarios.
|
||||
- No buy/sell recommendations and no instrument-specific advice, even if the
|
||||
user asks directly ("should I buy X?", "what about TICKER?"). Redirect to
|
||||
the regime and the fundamentals.
|
||||
- No forward price calls, no targets, no triggers, no "close above/below",
|
||||
no floors, no ceilings. The compliance rider above is in force in chat too.
|
||||
- The chat receives the latest log, live quotes, and headlines — it does NOT
|
||||
receive any portfolio or holdings context. If the user mentions their own
|
||||
positions, do not engage with them at the per-position level; answer the
|
||||
underlying macro question instead.
|
||||
- Keep the same audience and analysis discipline established above."""
|
||||
|
||||
|
||||
|
|
@ -305,7 +367,9 @@ def build_summary_system_prompt(tone: str, analysis: str) -> str:
|
|||
field is caught by the reviewer agent (services/output_review)."""
|
||||
tone_block = _TONE[_resolve_tone(tone)]
|
||||
analysis_block = _ANALYSIS.get(analysis.upper(), _ANALYSIS["SPECULATIVE"])
|
||||
return f"""You write a TINY interpretation (≤60 words, 2-3 sentences) \
|
||||
return f"""{_COMPLIANCE_RIDER}
|
||||
|
||||
You write a TINY interpretation (≤60 words, 2-3 sentences) \
|
||||
of ONE indicator group for a strategic markets dashboard.
|
||||
|
||||
# Output format (strict)
|
||||
|
|
@ -341,8 +405,10 @@ finished read, not the thinking.
|
|||
- Cite at most 2-3 specific numbers and ONLY when they anchor an \
|
||||
interpretation. Don't list moves; explain them.
|
||||
- Multi-week / multi-month horizon. 1-day moves under 2% are noise — skip.
|
||||
- No buy/sell language. No predictions. No watch list. No TL;DR. No date \
|
||||
header. No "system temperature" line — that belongs to the full daily log.
|
||||
- No buy/sell language. No price targets, no "close above/below", no \
|
||||
floors/ceilings/support/resistance, no triggers. No forward price calls on \
|
||||
named instruments. No watch list. No TL;DR. No date header. No "system \
|
||||
temperature" line — that belongs to the full daily log.
|
||||
|
||||
{tone_block}
|
||||
|
||||
|
|
@ -370,7 +436,9 @@ def build_aggregate_summary_system_prompt(tone: str, analysis: str) -> str:
|
|||
{"read": "..."} only; the field is the publishable text verbatim."""
|
||||
tone_block = _TONE[_resolve_tone(tone)]
|
||||
analysis_block = _ANALYSIS.get(analysis.upper(), _ANALYSIS["SPECULATIVE"])
|
||||
return f"""You write a single SHORT cross-asset INTERPRETATION (≤80 \
|
||||
return f"""{_COMPLIANCE_RIDER}
|
||||
|
||||
You write a single SHORT cross-asset INTERPRETATION (≤80 \
|
||||
words, 2-4 sentences) for the dashboard header. The reader is glancing — \
|
||||
give them the meaning of the whole tape, not a recap.
|
||||
|
||||
|
|
@ -406,7 +474,9 @@ parenthetical asides that question your own numbers.
|
|||
risk premium is in commodities but not vol". Cite no more than 3 specific \
|
||||
numbers, and only as anchors for the interpretation.
|
||||
- Multi-week / multi-month horizon. 1-day moves under 2% are noise.
|
||||
- No buy/sell language. No predictions of specific levels.
|
||||
- No buy/sell language. No forward price calls on named instruments. \
|
||||
No targets, floors, ceilings, support/resistance, "close above/below", or \
|
||||
trigger framing of any kind.
|
||||
|
||||
{tone_block}
|
||||
|
||||
|
|
@ -437,7 +507,13 @@ def build_chat_system_prompt(
|
|||
) -> str:
|
||||
"""Composed system prompt for the /log chat sidebar. Carries the user's
|
||||
chosen tone + analysis style and inlines the latest log + market data +
|
||||
headlines as reference material the model can cite from."""
|
||||
headlines as reference material the model can cite from.
|
||||
|
||||
Compliance contract: no holdings / portfolio / per-user position data may
|
||||
be passed to this builder. The signature deliberately exposes only log,
|
||||
quotes, and headlines — adding a holdings parameter would re-open the
|
||||
advice surface that Task 1 closed. If a future caller needs portfolio
|
||||
context, the right answer is to redesign the chat, not to bolt it on."""
|
||||
parts = [build_system_prompt(tone, analysis), "", _CHAT_OVERRIDES, ""]
|
||||
if reference_line:
|
||||
parts.append(f"# Doc reference snapshot\n{reference_line}\n")
|
||||
|
|
@ -539,11 +615,16 @@ def build_daily_digest_prompt(
|
|||
24h and looks forward to the upcoming session. Longer, less
|
||||
'live-blogging,' more contextual. Target ~600 words."""
|
||||
system = (
|
||||
f"{_COMPLIANCE_RIDER}\n\n"
|
||||
"You write the daily editorial digest for Read the Markets. "
|
||||
f"Audience tone: {tone.upper()}. {_digest_tone_clause(tone)} "
|
||||
"Cover: (1) what mattered yesterday, (2) what to watch in today's "
|
||||
"EU and US sessions, (3) one cross-asset thread connecting them. "
|
||||
"No predictions of price level, no buy/sell language. Target ~600 "
|
||||
"Cover: (1) what mattered yesterday, (2) what releases or events are "
|
||||
"scheduled in today's EU and US sessions, (3) one cross-asset thread "
|
||||
"connecting them. Frame (2) as scheduled events to be aware of, NOT "
|
||||
"as a price-watch list. "
|
||||
"No predictions of price level, no buy/sell language, no targets, "
|
||||
"no 'close above/below', no floors/ceilings/support/resistance, "
|
||||
"no trigger framing on named instruments. Target ~600 "
|
||||
"words. Output HTML using only <p>, <h3>, <ul>, <li>, <strong>, "
|
||||
"<em> — no <html>, <head>, or <body> wrapper, no inline styles."
|
||||
)
|
||||
|
|
@ -566,12 +647,16 @@ def build_weekly_digest_prompt(
|
|||
|
||||
Sent to ALL opt-in users (free and paid). Target ~900 words."""
|
||||
system = (
|
||||
f"{_COMPLIANCE_RIDER}\n\n"
|
||||
"You write the Sunday weekly digest for Read the Markets. "
|
||||
f"Audience tone: {tone.upper()}. {_digest_tone_clause(tone)} "
|
||||
"Cover: (1) the week behind — what moved and why, "
|
||||
"(2) the week ahead — releases, earnings, central-bank meetings, "
|
||||
"(2) the week ahead — releases, earnings, central-bank meetings as "
|
||||
"scheduled events, NOT as a list of price levels to watch, "
|
||||
"(3) the cross-asset story to keep in mind. "
|
||||
"No predictions of price level, no buy/sell language. Target ~900 "
|
||||
"No predictions of price level, no buy/sell language, no targets, "
|
||||
"no 'close above/below', no floors/ceilings/support/resistance, "
|
||||
"no trigger framing on named instruments. Target ~900 "
|
||||
"words. Output HTML using only <p>, <h3>, <ul>, <li>, <strong>, "
|
||||
"<em> — no <html>, <head>, or <body> wrapper, no inline styles."
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue