Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fdcac3b55 | ||
|
|
5ea0785be6 |
+1
-1
@@ -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=
|
||||||
|
|
||||||
|
|||||||
@@ -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 Mon–Fri) — the only full-universe qualifying observation:
|
**Near-close** (~15:30 ET Mon–Fri) — 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
@@ -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 = ""
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 ticker’s composite drops sharply' },
|
{ key: 'score_drop_enabled', label: 'Score deterioration', hint: 'a watched ticker’s 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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user