Global AI Research Dossiers


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.NS or RELIANCE.NS for NSE, 500182.BO for BSE, and AAPL or AAPL:NASDAQ for 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 dossier action is enabled, and the dossier can be queued.
  • Provider unreachable — the yfinance import fails or Ticker(...).get_info() raises (network error, rate limit, missing package). The UI shows provider verification is unavailable; no run is queued and the operator can retry later.
  • Empty payload — yfinance returned None or an empty dict for the requested symbol. The UI shows provider 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 exchange or provider 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.