Compare commits

..
2 Commits
Author SHA1 Message Date
dennisthiessenandClaude Opus 5 7fdcac3b55 docs: carry the risk-monitor wording through docs, comments and logs
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m12s
Deploy / deploy (push) Successful in 37s
Follows 5ea0785, which renamed the user-visible labels. This finishes the pass
so code, docs and operator output use one vocabulary: README (pipeline list,
route table, FRED row), the methodology doc title, .env.example and config
comments, the snapshot model / event-study / service / test docstrings, the
scheduler section headers and morning-pipeline docstring, the TopBar status
text ("bullish regime" -> "bullish trend"), and the four "Regime monitor:" log
prefixes.

Deliberately NOT changed, because "market regime" is also a standard finance
term and most occurrences are not this job: the backtest caveat "~6 months is
roughly one market regime" in backtest_service, README, BacktestPanel and every
generated reports/*.json; "a regime shift" in TrackRecordPanel; and the
capacity-bracket findings doc. Renaming those would have made the text wrong.

Also unchanged, being persisted or externally linked rather than wording: the
regime_monitor / market_regime job ids, the regime_quadrant_enabled setting key,
the /regime route, METHODOLOGY and the snapshot fields, the service/test module
filenames, and docs/research/regime-monitor-v3.md's path (referenced from commit
messages). The doc now carries a one-line note recording the old name and why
those identifiers still use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 22:51:49 +02:00
dennisthiessenandClaude Opus 5 5ea0785be6 refactor(ui): name the two regime jobs for what they actually do
"Market Regime" and "Regime Monitor" sat next to each other in Admin -> Jobs
(pipeline steps 4 and 5) reading as the same job. They are unrelated, and the
names had it backwards: "Market Regime" is the SPY 50/200 guard that drives the
TopBar trend dot and the counter-trend warning on setups, so it changes what a
setup shows; "Regime Monitor" is the observational AI/Tech thermometer that
explicitly feeds no trades. The more consequential job had the vaguer name.

  market_regime   "Market Regime"   -> "Market Trend (SPY)"
  regime_monitor  "Regime Monitor"  -> "AI/Tech Risk Monitor"

Display strings only. The job *ids* are persisted -- they key the pipeline step
list, cron config, runtime tracking and run history -- so they are untouched,
as is the /regime route, which keeps existing links working.

The label the admin UI renders comes from JOB_LABELS in admin_service (via
routers/jobs.py), not from the scheduler's APScheduler `name=`. Both are updated;
only the former is user-visible.

Carries the vocabulary through the rest of the surface so it does not half-land:
page title, nav ("Regime" -> "Risk"), the empty-state instruction that names the
job to run, the quadrant alert toggle, the morning-pipeline hint, and the
Telegram alert headline ("Regime quadrant change" -> "AI/Tech risk quadrant
change"). No test asserts any of these strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:48:15 +02:00
17 changed files with 43 additions and 33 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ SEC_REQUEST_SPACING_SECONDS=0.2
SEC_MAX_RETRIES=4 SEC_MAX_RETRIES=4
SEC_REQUEST_TIMEOUT_SECONDS=30.0 SEC_REQUEST_TIMEOUT_SECONDS=30.0
# Regime Monitor — FRED (VIX + HY credit spreads). Free key: https://fred.stlouisfed.org/docs/api/api_key.html # AI/Tech Risk Monitor — FRED (VIX + HY credit spreads). Free key: https://fred.stlouisfed.org/docs/api/api_key.html
# Optional: without it the volatility (V1) and credit (C1) pillars show as n/a. # Optional: without it the volatility (V1) and credit (C1) pillars show as n/a.
FRED_API_KEY= FRED_API_KEY=
+4 -4
View File
@@ -133,8 +133,8 @@ indicators.
1. **OHLCV** — latest daily bars (Alpaca); new tickers backfill ~5 years. 1. **OHLCV** — latest daily bars (Alpaca); new tickers backfill ~5 years.
2. **Sentiment** — stale names that matter (top-pick feeders, watchlist, open paper, discovery net). Display context only; the activation gate is price-only. 2. **Sentiment** — stale names that matter (top-pick feeders, watchlist, open paper, discovery net). Display context only; the activation gate is price-only.
3. **Market Regime** + **Regime Monitor** — breadth/trend and the v3 risk thermometer; feed no trades. 3. **Market Trend (SPY)** + **AI/Tech Risk Monitor** — the SPY trend guard and the v3 risk thermometer; feed no trades.
4. **Telegram alerts** — change-driven (regime-quadrant etc.); quiet days stay quiet. Setup alerts still fire on the near-close pipeline after the scan. 4. **Telegram alerts** — change-driven (risk-quadrant etc.); quiet days stay quiet. Setup alerts still fire on the near-close pipeline after the scan.
**Near-close** (~15:30 ET MonFri) — the only full-universe qualifying observation: **Near-close** (~15:30 ET MonFri) — the only full-universe qualifying observation:
@@ -351,7 +351,7 @@ Corollaries: never let an unvalidated score gate setups; the outcome evaluator m
| `/` | Dashboard — top setups, open trades, regime (default) | Authenticated | | `/` | Dashboard — top setups, open trades, regime (default) | Authenticated |
| `/market` | Market — watchlist + rankings tabs | Authenticated | | `/market` | Market — watchlist + rankings tabs | Authenticated |
| `/signals` | Signals — scanner + track record tabs | Authenticated | | `/signals` | Signals — scanner + track record tabs | Authenticated |
| `/regime` | Market Regime | Authenticated | | `/regime` | AI/Tech Risk Monitor | Authenticated |
| `/ticker/:symbol` | Ticker Detail | Authenticated | | `/ticker/:symbol` | Ticker Detail | Authenticated |
| `/admin` | Admin Panel | Admin only | | `/admin` | Admin Panel | Admin only |
@@ -583,7 +583,7 @@ Configure in `.env` (copy from `.env.example`):
| `OPENAI_API_KEY` | For sentiment (OpenAI path) | — | OpenAI API key | | `OPENAI_API_KEY` | For sentiment (OpenAI path) | — | OpenAI API key |
| `OPENAI_MODEL` | No | `gpt-4o-mini` | OpenAI model name | | `OPENAI_MODEL` | No | `gpt-4o-mini` | OpenAI model name |
| `OPENAI_SENTIMENT_BATCH_SIZE` | No | `5` | Micro-batch size for sentiment collector | | `OPENAI_SENTIMENT_BATCH_SIZE` | No | `5` | Micro-batch size for sentiment collector |
| `FRED_API_KEY` | Optional (regime) | — | FRED key for the regime monitor (VIX, credit spreads) | | `FRED_API_KEY` | Optional (risk monitor) | — | FRED key for the AI/Tech risk monitor (VIX, credit spreads) |
| `TELEGRAM_BOT_TOKEN` | Optional (alerts) | — | Telegram bot token for alerts (can also be set in Admin) | | `TELEGRAM_BOT_TOKEN` | Optional (alerts) | — | Telegram bot token for alerts (can also be set in Admin) |
| `TELEGRAM_CHAT_ID` | Optional (alerts) | — | Telegram chat id for alerts | | `TELEGRAM_CHAT_ID` | Optional (alerts) | — | Telegram chat id for alerts |
| `DATA_COLLECTOR_FREQUENCY` | No | `daily` | OHLCV collection schedule (legacy — see note below) | | `DATA_COLLECTOR_FREQUENCY` | No | `daily` | OHLCV collection schedule (legacy — see note below) |
+1 -1
View File
@@ -52,7 +52,7 @@ class Settings(BaseSettings):
sec_max_retries: int = 4 sec_max_retries: int = 4
sec_request_timeout_seconds: float = 30.0 sec_request_timeout_seconds: float = 30.0
# Regime Monitor — FRED (VIX level + HY credit spreads). Optional: without it # AI/Tech Risk Monitor — FRED (VIX level + HY credit spreads). Optional: without it
# the volatility (P5) and credit-spread (F2) signals are reported as n/a. # the volatility (P5) and credit-spread (F2) signals are reported as n/a.
fred_api_key: str = "" fred_api_key: str = ""
+1 -1
View File
@@ -8,7 +8,7 @@ from app.database import Base
class RegimeSnapshot(Base): class RegimeSnapshot(Base):
"""Daily point-in-time snapshot of the AI/Tech Regime Monitor. """Daily point-in-time snapshot of the AI/Tech Risk Monitor.
One row per calendar date (unique). ``breakdown_json`` holds the full One row per calendar date (unique). ``breakdown_json`` holds the full
``breakdown_json`` is authoritative for v2 State, Warning, source dates, ``breakdown_json`` is authoritative for v2 State, Warning, source dates,
+9 -5
View File
@@ -1057,7 +1057,7 @@ async def dispatch_alerts_job() -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Job: Market Regime # Job: Market Trend (SPY)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1116,7 +1116,7 @@ async def collect_benchmark() -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Job: Regime Monitor # Job: AI/Tech Risk Monitor
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -1396,7 +1396,7 @@ async def _run_pipeline(job_name: str, steps: list[tuple[str, str]]) -> None:
async def run_daily_pipeline() -> None: async def run_daily_pipeline() -> None:
"""Morning flow: OHLCV → benchmark → sentiment → market regime (no scan).""" """Morning flow: OHLCV → benchmark → sentiment → trend/risk (no scan)."""
await _run_pipeline("daily_pipeline", _DAILY_PIPELINE_STEPS) await _run_pipeline("daily_pipeline", _DAILY_PIPELINE_STEPS)
@@ -1561,8 +1561,12 @@ def configure_scheduler(schedule_config: dict[str, str] | None = None) -> None:
(scan_rr, "rr_scanner", "R:R Scanner"), (scan_rr, "rr_scanner", "R:R Scanner"),
(run_shadow_book, "shadow_book", "Shadow Book (auto-traded strategy)"), (run_shadow_book, "shadow_book", "Shadow Book (auto-traded strategy)"),
(evaluate_outcomes, "outcome_evaluator", "Outcome Evaluator"), (evaluate_outcomes, "outcome_evaluator", "Outcome Evaluator"),
(compute_market_regime, "market_regime", "Market Regime"), # Labels only -- the ids are persisted (pipeline steps, cron config, run
(compute_regime_monitor, "regime_monitor", "Regime Monitor"), # history), so they stay. "Market Regime"/"Regime Monitor" read as the
# same job and had it backwards besides: the SPY guard is the one that
# changes what a setup shows, while the monitor is observational.
(compute_market_regime, "market_regime", "Market Trend (SPY)"),
(compute_regime_monitor, "regime_monitor", "AI/Tech Risk Monitor"),
] ]
for fn, job_id, job_name in _members: for fn, job_id, job_name in _members:
scheduler.add_job( scheduler.add_job(
+3 -2
View File
@@ -639,8 +639,9 @@ JOB_LABELS = {
"ticker_universe_sync": "Ticker Universe Sync", "ticker_universe_sync": "Ticker Universe Sync",
"outcome_evaluator": "Outcome Evaluator", "outcome_evaluator": "Outcome Evaluator",
"alerts": "Alerts Dispatcher", "alerts": "Alerts Dispatcher",
"market_regime": "Market Regime", # Keys are persisted job ids and must not change; these are display only.
"regime_monitor": "Regime Monitor", "market_regime": "Market Trend (SPY)",
"regime_monitor": "AI/Tech Risk Monitor",
"event_study": "Event Study", "event_study": "Event Study",
"backtest": "Backtest", "backtest": "Backtest",
"daily_pipeline": "Morning Pipeline", "daily_pipeline": "Morning Pipeline",
+1 -1
View File
@@ -860,7 +860,7 @@ async def _collect_regime_quadrant(db: AsyncSession) -> list[tuple[str, str]]:
else: else:
metrics = f"State {x:.0f} · Warning {y:.0f}" metrics = f"State {x:.0f} · Warning {y:.0f}"
text = ( text = (
f"🧭 <b>Regime quadrant change</b>\n" f"🧭 <b>AI/Tech risk quadrant change</b>\n"
f"{QUAD_LABELS.get(prev, prev)}{QUAD_LABELS.get(new_q, new_q)}\n" f"{QUAD_LABELS.get(prev, prev)}{QUAD_LABELS.get(new_q, new_q)}\n"
f"{metrics}\n" f"{metrics}\n"
f"coverage: state {state.get('coverage'):.0f}% / warning {warning.get('coverage'):.0f}%\n" f"coverage: state {state.get('coverage'):.0f}% / warning {warning.get('coverage'):.0f}%\n"
+1 -1
View File
@@ -1,4 +1,4 @@
"""Compact chronological validation for the Regime Monitor warning score. """Compact chronological validation for the AI/Tech Risk Monitor warning score.
The study calls its outcome a 10% correction, uses the first 70% of sessions to The study calls its outcome a 10% correction, uses the first 70% of sessions to
freeze an 80th-percentile warning threshold, and reports alarm episodes only on freeze an 80th-percentile warning threshold, and reports alarm episodes only on
+5 -5
View File
@@ -1,4 +1,4 @@
"""AI/Tech Regime Monitor v3. """AI/Tech Risk Monitor v3.
The monitor is a risk thermometer, not a probability or trading rule. It keeps The monitor is a risk thermometer, not a probability or trading rule. It keeps
two deliberately separate outputs: two deliberately separate outputs:
@@ -918,7 +918,7 @@ async def _fetch_prices(config: dict, start: date, end: date) -> dict[str, Serie
bars = await provider.fetch_ohlcv(symbol, start, end) bars = await provider.fetch_ohlcv(symbol, start, end)
out[symbol] = sorted(((b.date, float(b.close)) for b in bars), key=lambda item: item[0]) out[symbol] = sorted(((b.date, float(b.close)) for b in bars), key=lambda item: item[0])
except Exception as exc: except Exception as exc:
logger.warning("Regime monitor: price fetch failed for %s: %s", symbol, exc) logger.warning("Risk monitor: price fetch failed for %s: %s", symbol, exc)
return out return out
@@ -941,7 +941,7 @@ async def _fetch_fred_series(series_id: str, start: date, end: date) -> Series |
response.raise_for_status() response.raise_for_status()
payload = response.json() payload = response.json()
except Exception as exc: except Exception as exc:
logger.warning("Regime monitor: FRED fetch failed for %s: %s", series_id, exc) logger.warning("Risk monitor: FRED fetch failed for %s: %s", series_id, exc)
return None return None
out: Series = [] out: Series = []
@@ -1026,7 +1026,7 @@ async def update_regime_monitor(
try: try:
overrides = await refresh_fundamental_overrides(db, config=config) overrides = await refresh_fundamental_overrides(db, config=config)
except Exception as exc: except Exception as exc:
logger.warning("Regime monitor: fundamentals refresh skipped: %s", exc) logger.warning("Risk monitor: fundamentals refresh skipped: %s", exc)
end = date.today() end = date.today()
prices = await _fetch_prices(config, end - timedelta(days=1200), end) prices = await _fetch_prices(config, end - timedelta(days=1200), end)
@@ -1050,7 +1050,7 @@ async def update_regime_monitor(
) )
divergence = breadth_service.compute_divergence_series(breadth, leader_series) divergence = breadth_service.compute_divergence_series(breadth, leader_series)
except Exception as exc: except Exception as exc:
logger.warning("Regime monitor: fixed-basket breadth skipped: %s", exc) logger.warning("Risk monitor: fixed-basket breadth skipped: %s", exc)
breadth, breadth_counts, divergence = {}, {}, {} breadth, breadth_counts, divergence = {}, {}, {}
latest_snapshot = await _latest_snapshot_row(db) latest_snapshot = await _latest_snapshot_row(db)
+6 -2
View File
@@ -1,6 +1,10 @@
# Regime Monitor v3 methodology # AI/Tech Risk Monitor v3 methodology
The Regime Monitor is an observational AI/Tech risk thermometer. It does not Named "Regime Monitor" until 2026-08-07; the filename, the `regime_monitor` job
id, the `/regime` route and the `METHODOLOGY`/snapshot fields keep the old word,
because those are persisted or externally linked. Only the wording changed.
The AI/Tech Risk Monitor is an observational risk thermometer. It does not
gate entries, exits, position size, ranking, or alerts about individual setups. gate entries, exits, position size, ranking, or alerts about individual setups.
v3 supersedes v2. Every parameter below was calibrated against the 408 v2 v3 supersedes v2. Every parameter below was calibrated against the 408 v2
@@ -21,7 +21,7 @@ const TRIGGERS: { key: TriggerKey; label: string; hint: string }[] = [
{ key: 'sr_proximity_enabled', label: 'Watchlist S/R proximity', hint: 'a watched ticker nears a strong support/resistance' }, { key: 'sr_proximity_enabled', label: 'Watchlist S/R proximity', hint: 'a watched ticker nears a strong support/resistance' },
{ key: 'score_drop_enabled', label: 'Score deterioration', hint: 'a watched tickers composite drops sharply' }, { key: 'score_drop_enabled', label: 'Score deterioration', hint: 'a watched tickers composite drops sharply' },
{ key: 'digest_enabled', label: 'Daily digest', hint: 'end-of-day summary incl. open trades + trailing stops' }, { key: 'digest_enabled', label: 'Daily digest', hint: 'end-of-day summary incl. open trades + trailing stops' },
{ key: 'regime_quadrant_enabled', label: 'Regime quadrant change', hint: 'the regime monitor shifts quadrant (hysteresis + cooldown)' }, { key: 'regime_quadrant_enabled', label: 'Risk quadrant change', hint: 'the AI/Tech risk monitor shifts quadrant (hysteresis + cooldown)' },
{ key: 'trade_closed_enabled', label: 'Trade closed', hint: 'a paper trade auto-closes (trailing/target/stop) — incl. losses' }, { key: 'trade_closed_enabled', label: 'Trade closed', hint: 'a paper trade auto-closes (trailing/target/stop) — incl. losses' },
]; ];
@@ -22,7 +22,7 @@ const FIELDS: { key: keyof ScheduleConfig; label: string; hint: string; mono?: b
{ {
key: 'schedule_daily_pipeline_cron', key: 'schedule_daily_pipeline_cron',
label: 'Morning pipeline', label: 'Morning pipeline',
hint: 'OHLCV → benchmark → sentiment → regime → alerts (no R:R scan). Default 02:00 ET so regime-quadrant changes hit Telegram in the morning.', hint: 'OHLCV → benchmark → sentiment → trend/risk → alerts (no R:R scan). Default 02:00 ET so risk-quadrant changes hit Telegram in the morning.',
mono: true, mono: true,
}, },
{ {
+1 -1
View File
@@ -7,7 +7,7 @@ const navItems = [
{ to: '/', label: 'Overview', end: true }, { to: '/', label: 'Overview', end: true },
{ to: '/market', label: 'Market', end: false }, { to: '/market', label: 'Market', end: false },
{ to: '/signals', label: 'Signals', end: false }, { to: '/signals', label: 'Signals', end: false },
{ to: '/regime', label: 'Regime', end: false }, { to: '/regime', label: 'Risk', end: false },
]; ];
export default function MobileNav() { export default function MobileNav() {
+4 -3
View File
@@ -13,7 +13,8 @@ const navItems = [
{ to: '/', label: 'Overview', end: true }, { to: '/', label: 'Overview', end: true },
{ to: '/market', label: 'Market', end: false }, { to: '/market', label: 'Market', end: false },
{ to: '/signals', label: 'Signals', end: false }, { to: '/signals', label: 'Signals', end: false },
{ to: '/regime', label: 'Regime', end: false }, // Route stays /regime so existing links keep working; only the label changes.
{ to: '/regime', label: 'Risk', end: false },
]; ];
const linkClasses = (isActive: boolean) => const linkClasses = (isActive: boolean) =>
@@ -84,7 +85,7 @@ export default function TopBar() {
</div> </div>
<div className="ml-auto flex items-center gap-5"> <div className="ml-auto flex items-center gap-5">
{/* Market regime — ambient status; the full picture lives on /regime */} {/* SPY trend — ambient status; the full picture lives on /regime */}
{regime.data && ( {regime.data && (
<NavLink <NavLink
to="/regime" to="/regime"
@@ -99,7 +100,7 @@ export default function TopBar() {
> >
<span className={`inline-block h-1.5 w-1.5 rounded-full ${regimeDot(regime.data.label)}`} /> <span className={`inline-block h-1.5 w-1.5 rounded-full ${regimeDot(regime.data.label)}`} />
<span className="text-[11px] capitalize text-gray-500 transition-colors group-hover:text-gray-300"> <span className="text-[11px] capitalize text-gray-500 transition-colors group-hover:text-gray-300">
{regime.data.label} regime {regime.data.label} trend
</span> </span>
</NavLink> </NavLink>
)} )}
+1 -1
View File
@@ -36,7 +36,7 @@ export function regimeHeadline(r: MarketRegime): string {
return `${b} ${r.label}${pct}`; return `${b} ${r.label}${pct}`;
} }
/** Whether a setup direction fights the prevailing market regime. */ /** Whether a setup direction fights the prevailing SPY trend. */
export function isCounterTrend(direction: string, label: MarketRegime['label']): boolean { export function isCounterTrend(direction: string, label: MarketRegime['label']): boolean {
if (label === 'bullish') return direction === 'short'; if (label === 'bullish') return direction === 'short';
if (label === 'bearish') return direction === 'long'; if (label === 'bearish') return direction === 'long';
+2 -2
View File
@@ -507,13 +507,13 @@ export default function RegimePage() {
return ( return (
<div className="space-y-6 animate-slide-up"> <div className="space-y-6 animate-slide-up">
<PageHeader <PageHeader
title="Regime Monitor" title="AI/Tech Risk Monitor"
subtitle="AI/Tech risk thermometer — observational only, feeds no entry, exit, or sizing decision" subtitle="AI/Tech risk thermometer — observational only, feeds no entry, exit, or sizing decision"
/> />
{monitor.isLoading && <><SkeletonCard className="h-44" /><SkeletonTable rows={6} cols={4} /></>} {monitor.isLoading && <><SkeletonCard className="h-44" /><SkeletonTable rows={6} cols={4} /></>}
{monitor.isError && <Callout variant="error" onRetry={() => monitor.refetch()}>Failed to load: {(monitor.error as Error).message}</Callout>} {monitor.isError && <Callout variant="error" onRetry={() => monitor.refetch()}>Failed to load: {(monitor.error as Error).message}</Callout>}
{data && !data.available && <Callout variant="empty">Not computed yet run Regime Monitor from Admin Jobs or wait for the daily pipeline.</Callout>} {data && !data.available && <Callout variant="empty">Not computed yet run AI/Tech Risk Monitor from Admin Jobs or wait for the daily pipeline.</Callout>}
{data?.available && data.state && data.warning && ( {data?.available && data.state && data.warning && (
<> <>
+1 -1
View File
@@ -1,4 +1,4 @@
"""Pure-function tests for the v3 Regime Monitor contract.""" """Pure-function tests for the v3 AI/Tech Risk Monitor contract."""
from __future__ import annotations from __future__ import annotations