title: "Global AI Research Dossiers" section: "ai-research" slug: "ai-research"
Global AI Research Dossiers
The Global AI Research Dossiers launcher lets an authenticated operator
queue an advisory AI Portfolio Research dossier for a listed Indian or US
equity that is not in a holding or watchlist. It shares the existing
AIResearchOrchestrator, daily-run cap, per-run cost cap, evidence
ledger, and publication QA, so dossiers launched from here look identical
to dossiers launched from a holding detail page once the worker
processes them. The launcher is read-only and advisory: it never mutates
holdings, watchlists, transactions, broker mappings, or orders, and it
never places a trade or changes trading settings.
Capability flags
| Flag | Effect |
|---|---|
| PORTFOLIO_AI_RESEARCH_ENABLED=1 | capability on; API live |
| PORTFOLIO_AI_RESEARCH_WORKER_ENABLED=1 | background worker polls for queued runs |
| PORTFOLIO_AI_RESEARCH_DAILY_RUN_LIMIT=25 | cap on new global dossiers per day (replays do not count) |
The worker, schedule, and outcome evaluation flags are unchanged and apply to global dossiers as they do to holding-anchored dossiers.
Operator entry points
AI Research Center (dashboard)
Inside the AI Research Center, the Dossiers tab begins with a
Research any listed company card. The helper text reads:
Enter an India or US listed symbol. Use
CDSL.NSorRELIANCE.NSfor NSE,500182.BOfor BSE, andAAPLorAAPL:NASDAQfor US equities. This creates an advisory dossier only: it does not add a holding or watchlist entry, place orders, or change trading settings.
The operator types a symbol and optionally selects an exchange. Existing
registry matches appear before external resolution; a resolved result
shows the display name, exchange, country, currency, and the exact
provider ticker. The primary Run dossier action is disabled until the
identity is confirmed. The confirmation queues a normal CURRENT run and
opens the dossier. If resolution is ambiguous the UI requires an
exchange selection; if provider verification fails the UI shows the
reason and offers no run button. Existing holding/watchlist research
surfaces are unchanged.
Telegram
The authenticated /ai_research command accepts one symbol and an
optional supported exchange:
/ai_research AAPL NASDAQ
/ai_research CDSL.NS
/ai_research 500182.BO
The command calls the same resolver service as the dashboard, returns the resolved identity and queued run id, and links to the dossier when a configured dashboard URL is available. It does not accept portfolio, order, policy, broker, or live-mode parameters, and it cannot reach any execution adapter.
Supported examples
The parser is deterministic and closed. These are the supported shapes:
| Exchange | Input | Canonical symbol | Provider symbol |
| --- | --- | --- | --- |
| NSE | CDSL.NS | CDSL | CDSL.NS |
| NSE | RELIANCE + NSE | RELIANCE | RELIANCE.NS |
| BSE | 500182.BO | 500182 | 500182.BO |
| NASDAQ | AAPL + NASDAQ | AAPL | AAPL |
| NASDAQ | AAPL:NASDAQ | AAPL | AAPL |
| NYSE | IBM + NYSE | IBM | IBM |
| NYSE | IBM:NYSE | IBM | IBM |
A bare unqualified symbol without a supported exchange is rejected; the
provider cannot be asked to guess an exchange from prose. The resolver
accepts an explicit exchange on the same request and the parser
rejects any dot or colon suffix outside the closed map (.NS, .BO,
:NASDAQ, :NYSE). A suffix that conflicts with the explicit
exchange is rejected as a conflict.
Provider verification and unavailable states
The resolver asks yfinance to confirm the candidate symbol. The
verified response must include a non-empty symbol that matches the
canonical provider symbol, an exchange that maps to the requested
exchange (NSE, BSE, NASDAQ, NYSE), and a quote_type of
EQUITY. Anything else is rejected. The user-facing states are:
- Verified — display name, exchange, country, and currency are
shown, the
Run dossieraction is enabled, and the dossier can be queued. - Provider unreachable — the
yfinanceimport fails orTicker(...).get_info()raises (network error, rate limit, missing package). The UI showsprovider verification is unavailable; no run is queued and the operator can retry later. - Empty payload — yfinance returned
Noneor an empty dict for the requested symbol. The UI showsprovider returned no instrument identity; no run is queued and the operator should verify the ticker or pick a different exchange. - Ambiguous or conflicting identity — the verified symbol differs
from the requested canonical symbol, or the verified exchange does
not match the requested exchange. The UI shows
provider returned an ambiguous or conflicting exchangeorprovider returned an unverified instrument identity; no run is queued. - Unsupported asset class — ETFs, mutual funds, derivatives, and
crypto all fail closed. The UI shows
provider returned an unsupported or unverified equity; no run is queued. - Conflict with existing registry — a canonical record already
exists with a different
provider_symbol; the resolver raises without overwriting. The UI surfaces the conflict message and no run is queued.
Research-only registry storage
The launcher's only write to the instrument registry is an
Instrument row and a GLOBAL_AI_RESEARCH InstrumentAlias row
(audit_reason='GLOBAL_AI_RESEARCH_VERIFIED', created_by=actor).
The Instrument row carries the canonical symbol, exchange, country,
quote currency, provider, and provider_symbol. The alias stores
the normalized canonical symbol the resolver derived from the
input — for example, the canonical CDSL for the input CDSL.NS,
or the canonical RELIANCE for the input RELIANCE.NS. The raw
user input is never persisted as the alias, so a follow-up alias
lookup matches the persisted identity rather than the prose. Subsequent
resolves for the same (canonical_symbol, exchange) are idempotent
and return the existing Instrument without consulting the provider
again. The launcher never writes a Holding, Transaction,
WatchlistItem, broker connection, broker symbol map, broker snapshot,
or order; the launcher's static and runtime import guards (see
tests/portfolio_app/ai_research/test_safety_boundary.py and
tests/test_command_handler_ai_research.py::test_ai_research_module_never_imports_live_execution)
make sure the module never reaches broker, execution, scanner,
policies, or token-refresh surfaces.
CURRENT-only limitation
The first release queues CURRENT dossiers only. Historical global
runs are intentionally out of scope: the global launcher rejects
mode='HISTORICAL' with a 422 and a clear error, and the Telegram
command does not accept a cutoff timestamp.
The dashboard randomises the caller-supplied idempotency key on every
launch (globalThis.crypto.randomUUID() per click), so two distinct
dashboard actions for the same identity produce two distinct calls. The
Telegram command has no caller-supplied key: it deterministically derives
the idempotency key from the normalized (symbol, exchange, actor) identity
and resolved request fingerprint. Repeating the same Telegram command as the
same actor therefore returns the original run with created: false; changing
the symbol, exchange, or actor produces a different key.
For dashboard requests, server-side deduplication is scoped to the same
caller key + fingerprint combination: when the route sees the same caller
key replayed, it hashes the (caller_key, actor, fingerprint) tuple into a
server-owned namespaced idempotency key and returns the original run with
created: false. A different caller key (a fresh launch) always produces a
fresh namespaced key and therefore a fresh run. The configured daily cap
counts only new runs; replays of an existing run return the original row
with created: false.
Advisory boundary
A global dossier is strictly advisory. The launcher must not be used to add a company to a holding or watchlist, place an order, change a policy, or size a position. After the dossier completes the operator opens the dossier detail, reviews the rating, confidence, thesis, bull and bear cases, catalysts, risks, invalidation conditions, claims, and sources, and decides what — if anything — to do next. Adding a symbol to a holding, watchlist, or promotion pipeline is a separate operator action taken in the holding/watchlist surface or the policies surface, not in the AI Research Center.
Failure modes and operator next steps
| Symptom | Cause | Next step |
|---|---|---|
| 422 unsupported exchange | the request sent an exchange outside NSE/BSE/NASDAQ/NYSE | retry with a supported exchange or drop the field |
| 422 unqualified symbol requires an explicit supported exchange | bare symbol without an exchange | retry with an exchange or a recognized suffix |
| 422 suffix implies exchange=… but explicit exchange=… (conflict) | suffix and explicit exchange disagree | retry with a consistent pair |
| 422 provider verification is unavailable | yfinance is down or rate-limited | retry later; no state was mutated |
| 422 provider returned no instrument identity | the ticker does not exist on the requested exchange | check the symbol or pick a different exchange |
| 422 provider returned an unsupported or unverified equity | ETFs, funds, derivatives, crypto | not supported in the first release |
| 422 mode must be CURRENT or HISTORICAL (or historical-mode rejection) | historical mode was sent | drop the field; the launcher is CURRENT-only |
| 429 daily AI Research run limit reached | the configured daily cap is exhausted | wait until the IST day rolls over, or raise PORTFOLIO_AI_RESEARCH_DAILY_RUN_LIMIT |
| 409 same idempotency key with a different fingerprint | a different operator or symbol reused the key | pick a fresh idempotency key |
The end-to-end safety invariant — canonical instrument → queued run →
provider-symbol evidence request, with no financial state mutation —
is pinned by
tests/portfolio_app/ai_research/test_global_research_e2e.py.