The financial impact graph, in Python.
News → who it touches and why — the causal channel, the story's lean, and the
ripple — across stocks, commodities, FX, crypto, and private companies.
A research, screening & feature layer — direction is the news's content lean, not a price forecast.
▶ Live playground · Docs & pricing · Sample dataset
pip install marketdx # add [pandas] for .to_df(): pip install "marketdx[pandas]"An API key is required. Create a free one at https://marketdx.lab.ai (sign in → API keys), then pass it to the client (keep it out of source control — read it from an env var / secret in real apps):
from marketdx import MarketDX
mdx = MarketDX(api_key="avn_live_…") # your key from https://marketdx.lab.ai
for s in mdx.news(megatrend="ai-power", impact="indirect"):
print(s.title, [(e.name, e.impact.net_direction) for e in s.entities])That's the whole graph: every news event, every affected entity, labeled with direction, relevance, the causal aspect (the why), and whether it's the epicenter or a ripple — across five asset classes, including private companies ticker feeds can't see.
No key yet? Explore everything with zero signup in the playground, then grab a free key at https://marketdx.lab.ai. Every request is authenticated with your key (
Authorization: Bearer <key>) and metered in credits.
- Typed graph —
signal.entities[0].impact.aspects[0].directionwith autocomplete, not raw dicts. - Auto-pagination —
for s in mdx.news(...)pages for you.limit=caps how many you get (default 50;limit=Nonewalks the whole match set);page.totalis the full count. - Names, not ids —
megatrend="ai-power"or"AI Power & Cooling"or10040000all work. .to_df()— the whole result as a pandas DataFrame, one row per (event × entity × aspect).- Typed errors —
AuthError,QuotaError,RateLimitError,BadRequestError,NotFoundError.
# 1. Ripple: a themed event that also touches NON-thematic entities (our differentiator)
for s in mdx.news(megatrend="semiconductors", impact="indirect", limit=50):
...
# 2. Beyond tickers: private companies in a trend (OpenAI, Anthropic, Ampere, ChangXin…)
for c in mdx.megatrends("semiconductors").off_coverage():
print(c.name, c.type, "→", c.megatrend["node_name"])
# 2b. …or just the NEWS that moves private companies — one server-side filter on the feed
for s in mdx.news(entity_type="private", only_scored=True): # also: crypto / commodity / forex / stock
print(s.title, [e.name for e in s.entities if e.type == "private"])
# 3. Per-stock impact timeline + its news-derived rivals
tl = mdx.stock("NVDA.US").news(aspect="competition")
peers = mdx.stock("NVDA.US").competitors()
# 3b. Find ANY entity — stock, commodity, crypto, forex, or an off-coverage PRIVATE company —
# each hit carries a ready-to-use .ticker (+ .type) you feed straight back into by-tickers
gold = mdx.stocks(q="gold").first().ticker # -> "GOLD" (the commodity)
openai = mdx.stocks(q="openai").first().ticker # -> "oc:52" (private, off-coverage)
news = mdx.news_by_tickers(["NVDA.US", openai]) # NVIDIA + OpenAI news in one feed
# 4. News-driven screen — where the news leans positive on a theme (the model's read, for research)
positive_lean = mdx.stocks(megatrend="ai-power", direction="pos", country="US", order_by="news_count")
# 5. Semantic search — match news by MEANING, not keywords (each hit is explainable)
for s in mdx.news_search("chip export controls to China", limit=5):
print(round(s.similarity, 2), s.scored, s.title) # how-strongly-matched + does-it-move-an-entity
# 6. Brief in ONE call — pulse timeseries + top stories + winners/losers + heatmap + assets.
# Scope by theme, market, news-type, impact-channel, GICS sector, or any mix (>=1 scope).
brief = mdx.theme("ai-power").summary(window="qtd") # theme brief (== brief(megatrend="ai-power"))
brief = mdx.brief(news_type="commodity_supply", country="JP", window="90d") # "commodity news, in Japan"
brief = mdx.brief(gics="2550", country=["GB","DE","FR","IT","ES","NL"]) # "European retail pulse" (sector x region)
# Find a GICS code by name — mdx.gics() returns the whole bounded taxonomy (filter client-side):
hits = [(g.code, g.name) for g in mdx.gics() if "retail" in g.name.lower()] # -> ('2550','Consumer Discretionary Distribution & Retail'), …
print(brief["pulse"]["story_count"], brief["pulse"]["net_direction"])
print([w["ticker"] for w in brief["winners"]], "vs", [l["ticker"] for l in brief["losers"]])
print([a["name"] for a in brief["top_assets"]]) # commodity / forex / crypto the theme moves.to_df() returns the same columns as the sample dataset (impact-signals.csv) — so
anything you prototyped on the free CSV runs unchanged on the live graph:
df = mdx.news(megatrend="ai-power", impact="indirect").to_df()
# published_at · title · brief_text · entity_name · entity_ticker · entity_type · direction ·
# aspect · reason · relevance · impact · impact_score · node_name · entity_country · publisher · url
df.groupby(["entity_type", "direction"]).size() # who the news lands on, +/− by asset class (the model's read)
df[df.aspect == "tariff"].entity_name.value_counts() # who the tariff channel touchesFeed filters are EVENT-level, not row-level. news(direction=…, aspect=…, news_type=…, country=…)
selects articles that contain at least one matching impact and returns the whole article with
all its entities and aspects. So direction="pos" can return an article that also moves something neg,
and aspect="supply" can return one whose other entities are hit via monetary. For exact per-row
filtering, post-filter the entities:
for s in mdx.news(news_type="commodity_supply", direction="pos"):
for e in s.entities:
for a in (e.impact.aspects if e.impact else []):
if e.type == "commodity" and a.direction == "pos" and a.aspect == "supply":
... # exact row you asked forTwo different "direct" axes (don't conflate):
Signal.impact_type(direct|indirect) = the article's relation to the queried node — epicenter (direct) vs ripple (indirect). Set bynews(megatrend=…, impact="indirect").Entity.direct(True|False) = whether that entity is factually mentioned in the article (True) vs impact-only / not named (False).
Narrow the feed by entity — server-side. news() (and news_search()) filter by
entity_type / only_scored / min_relevance in the API, so page.total stays the exact filtered
count (no wasted paging). These keep the whole article — for an exact per-row cut, still post-filter
the entities as above.
mdx.news(entity_type="commodity") # feed → only stories that move a commodity
mdx.news(entity_type="private") # only stories moving a private co (OpenAI, SpaceX)
mdx.news(only_scored=True) # drop mention-only articles (keep judged impact)
mdx.news(min_relevance=0.8) # only a strongly-relevant scored entity
mdx.news(megatrend="ai-power", entity_type="crypto") # entity filters compose with megatrend scope
mdx.news_search("oil supply shock", entity_type="commodity") # search supports entity_type too
mdx.news_by_tickers("NVDA.US") # a ticker's news (direct + indirect by default)
mdx.news_by_tickers("NVDA.US", link="direct") # only where NVDA is factually named (not ripple-only)
mdx.news_search("oil shock", megatrend="ai-power") # semantic search, scoped to a trend
mdx.megatrends("ai-power").off_coverage() # private / off-coverage rosterEntity filters do not apply to
impact="indirect"(the ripple feed) — the API returns 400 if you combine them. Onnews_by_tickers, the ticker set already scopes the entities.
Only entities with a scored impact (many are mentioned-only) — only_scored=True narrows to such
articles server-side; then read the scored entities off each signal:
scored = [e for s in mdx.news(megatrend="ai-power", only_scored=True)
for e in s.entities if e.impact and e.impact.aspects]What to expect in entities (not bugs):
- Mostly mention-only. An article's
entitiesmix two kinds: mentioned (e.direct is True,e.impact is None) — every ticker the article names — and scored (e.impact is not None) — the ones the model judged materially moved. Mentions usually outnumber scored (a story names many tickers but moves a few). Want just the movers?only_scored=Trueor filtere.impact. entitiescan be empty.include=entitiesattaches entities if the article maps to any; a macro / policy / commodity story that names no covered company (e.g. "China bans helium exports") legitimately returnsentities == []. It means no entity resolved, not a dropped/failed enrich.
Provenance. A scored impact ships its evidence, not just a label: aspect+direction+relevance
(the judgment), reason (why), Entity.direct (named vs affected-only), and impact.label_version — the
labeling-scheme version (currently "1.0"), a per-label stamp that bumps when the model/prompt/taxonomy
changes so you can detect and re-evaluate shifts. Audit or gate on it: e.impact.label_version.
stock(t).news() is a stock-centric timeline — a StockNews (the stock's own impact /trend/
relevance), not an entity graph (no entities[]). For the full graph of an article, use news().
Story-collapse (on by default). The same story is often republished / rewritten across outlets.
Every news feed — news(), news_search(), news_by_tickers() and stock(t).news() — merges those
near-duplicates into a single signal by default (cosine-similarity grouping, server-side) so a feed
reads one-story-one-row. Pass collapse=False when you want the raw, un-deduped stream — e.g. to
measure coverage volume:
merged = mdx.news(megatrend="ai-power").to_list() # deduped (default)
raw = mdx.news(megatrend="ai-power", collapse=False).to_list() # every republicationEach collapsed signal also carries its cluster metadata — group a dashboard by story_id and see reach +
lifespan without double-counting:
for s in mdx.news(megatrend="ai-power", limit=5):
print(s.story_id, s.dup_count, s.first_seen, s.latest_seen) # cluster id · outlets · broke · last echomdx.theme(id).summary(...) (a theme = a megatrend node; also mdx.megatrends(id).summary(...)) returns
a pre-composed analyst brief so you don't stitch 5+ requests together. It's a fixed composite dict,
not a paginated list:
brief = mdx.theme("ai-power").summary(window="30d") # 7d/30d/90d/180d/1y or mtd/qtd/ytd (or from_/to)
brief["pulse"] # story_count, net_direction, pos/neg share + a `series` (volume+sentiment/bucket)
brief["top_stories"] # epicenter, deduped; market-wraps & no-member-named stories deprioritized
brief["ripple"] # indirect (ripple-in) stories, each with `via`
brief["winners"], brief["losers"] # member stocks by net direction
brief["aspect_heatmap"] # which channels the theme is playing out through
brief["top_entities"] # operating companies most in the news
brief["top_assets"] # commodity / forex / crypto the theme moves (split out from companies)Every count is story-deduped (20 outlets on one story = 1). The pulse.series is the momentum signal —
there's no single momentum scalar (the latest bucket is the current, partial period). Cost: 15 credits.
Any scope, not just a theme. mdx.brief(...) composes the same object over any AND-combination of
megatrend / news_type / country / aspect / gics (≥1 required) — a theme, a market, a news category,
an impact channel, a GICS sector, or a mix:
mdx.brief(country="JP") # how is Japan doing right now?
mdx.brief(news_type="commodity_supply", country="JP") # commodity news, in Japan
mdx.brief(aspect="tariff", window="90d") # everything moving via tariffs
mdx.brief(megatrend="semiconductors", country="US") # a theme, narrowed to one marketapplied_scope echoes what you filtered. node + ripple appear only when a single megatrend
anchors the brief (without a theme there's no ripple). Under a megatrend scope winners/losers are the
theme's members; otherwise they're the top +/- companies in the scope. theme(id).summary(...) ==
brief(megatrend=id, ...). megatrend/news_type/country/aspect/gics each take one value or a list.
Every call carries X-Credits-Charged / X-RateLimit-*; check your balance any time (free, unmetered):
mdx.account() # {'plan': …, 'credits': {'balance', 'daily_quota', 'resets_at', 'unlimited'}, 'rate_limit': …}from marketdx.errors import QuotaError, RateLimitError
try:
signals = mdx.news(megatrend="ai-power").to_list()
except RateLimitError as e:
time.sleep(e.retry_after or 1)
except QuotaError:
... # daily quota spent — resets 00:00 UTCEnum values (aspect, direction, entity_type, …) are type hints for your editor — the API is the
source of truth, so new values work without upgrading the SDK. The live list: mdx.enums().
news · news_search · news_types · megatrends (.stocks / .off_coverage) · gics (.stocks) ·
stocks (search + screener) · stock (.news / .competitors / .peers) · enums · account.
Full API docs: marketdx.lab.ai.
MIT. Built by MarketDX — democratizing financial data.