marketiv 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ .gen/
2
+ .secrets/
3
+ *.key.json
4
+ .env
5
+ __pycache__/
6
+ *.pyc
7
+ .DS_Store
8
+
9
+ # Python build artifacts + virtualenvs
10
+ .venv/
11
+ venv/
12
+ dist/
13
+ build/
14
+ *.egg-info/
15
+ .pytest_cache/
16
+ .mypy_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MarketIV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,290 @@
1
+ Metadata-Version: 2.5
2
+ Name: marketiv
3
+ Version: 0.16.0
4
+ Summary: The MarketIV financial impact graph, in Python — news→market impact (why, who, direction, ripple) across stocks, commodities, FX, crypto, and private companies.
5
+ Project-URL: Homepage, https://marketiv.app
6
+ Project-URL: Playground, https://marketiv.app/playground
7
+ Project-URL: Repository, https://github.com/MarketIV/marketiv
8
+ Project-URL: Sample dataset, https://github.com/MarketIV/marketiv/tree/main/datasets
9
+ Author-email: MarketIV <hello@marketdx.lab.ai>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,finance,impact-graph,market-impact,megatrends,news,quant,sdk
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: Office/Business :: Financial :: Investment
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: httpx>=0.24
21
+ Provides-Extra: dev
22
+ Requires-Dist: frictionless>=5; extra == 'dev'
23
+ Requires-Dist: mypy>=1.0; extra == 'dev'
24
+ Requires-Dist: pandas>=1.3; extra == 'dev'
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Provides-Extra: pandas
27
+ Requires-Dist: pandas>=1.3; extra == 'pandas'
28
+ Description-Content-Type: text/markdown
29
+
30
+ <h1 align="center">marketiv</h1>
31
+
32
+ <p align="center">
33
+ <b>The financial impact graph, in Python.</b><br>
34
+ News → who it <i>touches</i> and <i>why</i> — the causal channel, the story's <i>lean</i>, and the
35
+ <i>ripple</i> — across stocks, commodities, FX, crypto, <b>and private companies</b>.<br>
36
+ A research, screening &amp; feature layer — direction is the news's content lean, not a price forecast.
37
+ </p>
38
+
39
+ <p align="center">
40
+ <a href="https://marketiv.app/playground"><b>▶ Live playground</b></a> ·
41
+ <a href="https://marketiv.app">Docs & pricing</a> ·
42
+ <a href="https://github.com/MarketIV/marketiv/tree/main/datasets">Sample dataset</a>
43
+ </p>
44
+
45
+ ---
46
+
47
+ ```bash
48
+ pip install marketiv # add [pandas] for .to_df(): pip install "marketiv[pandas]"
49
+ ```
50
+
51
+ **An API key is required.** Create a free one at **https://marketiv.app** (sign in → *API keys*), then
52
+ pass it to the client (keep it out of source control — read it from an env var / secret in real apps):
53
+
54
+ ```python
55
+ from marketiv import MarketIV
56
+
57
+ mdx = MarketIV(api_key="avn_live_…") # your key from https://marketiv.app
58
+ for s in mdx.news(megatrend="ai-power", impact="indirect"):
59
+ print(s.title, [(e.name, e.impact.net_direction) for e in s.entities])
60
+ ```
61
+
62
+ That's the whole graph: every news event, every affected entity, labeled with **direction**,
63
+ **relevance**, the causal **aspect** (the *why*), and whether it's the **epicenter** or a **ripple** —
64
+ across five asset classes, **including private companies** ticker feeds can't see.
65
+
66
+ > **No key yet?** Explore everything with zero signup in the [playground](https://marketiv.app/playground),
67
+ > then grab a free key at **[https://marketiv.app](https://marketiv.app)**. Every request is
68
+ > authenticated with your key (`Authorization: Bearer <key>`) and metered in credits.
69
+
70
+ ## Why the SDK (not just `requests`)
71
+
72
+ - **Typed graph** — `signal.entities[0].impact.aspects[0].direction` with autocomplete, not raw dicts.
73
+ - **Auto-pagination** — `for s in mdx.news(...)` pages for you. `limit=` caps how many you get
74
+ (default 50; `limit=None` walks the whole match set); `page.total` is the full count.
75
+ - **Names, not ids** — `megatrend="ai-power"` or `"AI Power & Cooling"` or `10040000` all work.
76
+ - **`.to_df()`** — the whole result as a pandas DataFrame, one row per *(event × entity × aspect)*.
77
+ - **Typed errors** — `AuthError`, `QuotaError`, `RateLimitError`, `BadRequestError`, `NotFoundError`.
78
+
79
+ ## The graph, a few ways
80
+
81
+ ```python
82
+ # 1. Ripple: a themed event that also touches NON-thematic entities (our differentiator)
83
+ for s in mdx.news(megatrend="semiconductors", impact="indirect", limit=50):
84
+ ...
85
+
86
+ # 2. Beyond tickers: private companies in a trend (OpenAI, Anthropic, Ampere, ChangXin…)
87
+ for c in mdx.megatrends("semiconductors").off_coverage():
88
+ print(c.name, c.type, "→", c.megatrend["node_name"])
89
+
90
+ # 2b. …or just the NEWS that moves private companies — one server-side filter on the feed
91
+ for s in mdx.news(entity_type="private", only_scored=True): # also: crypto / commodity / forex / stock
92
+ print(s.title, [e.name for e in s.entities if e.type == "private"])
93
+
94
+ # 3. Per-stock impact timeline + its news-derived rivals
95
+ tl = mdx.stock("NVDA.US").news(aspect="competition")
96
+ peers = mdx.stock("NVDA.US").competitors()
97
+
98
+ # 3b. Find ANY entity — stock, commodity, crypto, forex, or an off-coverage PRIVATE company —
99
+ # each hit carries a ready-to-use .ticker (+ .type) you feed straight back into by-tickers
100
+ gold = mdx.stocks(q="gold").first().ticker # -> "GOLD" (the commodity)
101
+ openai = mdx.stocks(q="openai").first().ticker # -> "oc:52" (private, off-coverage)
102
+ news = mdx.news_by_tickers(["NVDA.US", openai]) # NVIDIA + OpenAI news in one feed
103
+
104
+ # 4. News-driven screen — where the news leans positive on a theme (the model's read, for research)
105
+ positive_lean = mdx.stocks(megatrend="ai-power", direction="pos", country="US", order_by="news_count")
106
+
107
+ # 5. Semantic search — match news by MEANING, not keywords (each hit is explainable)
108
+ for s in mdx.news_search("chip export controls to China", limit=5):
109
+ print(round(s.similarity, 2), s.scored, s.title) # how-strongly-matched + does-it-move-an-entity
110
+
111
+ # 6. Brief in ONE call — pulse timeseries + top stories + winners/losers + heatmap + assets.
112
+ # Scope by theme, market, news-type, impact-channel, GICS sector, or any mix (>=1 scope).
113
+ brief = mdx.theme("ai-power").summary(window="qtd") # theme brief (== brief(megatrend="ai-power"))
114
+ brief = mdx.brief(news_type="commodity_supply", country="JP", window="90d") # "commodity news, in Japan"
115
+ brief = mdx.brief(gics="2550", country=["GB","DE","FR","IT","ES","NL"]) # "European retail pulse" (sector x region)
116
+ # Find a GICS code by name — mdx.gics() returns the whole bounded taxonomy (filter client-side):
117
+ hits = [(g.code, g.name) for g in mdx.gics() if "retail" in g.name.lower()] # -> ('2550','Consumer Discretionary Distribution & Retail'), …
118
+ print(brief["pulse"]["story_count"], brief["pulse"]["net_direction"])
119
+ print([w["ticker"] for w in brief["winners"]], "vs", [l["ticker"] for l in brief["losers"]])
120
+ print([a["name"] for a in brief["top_assets"]]) # commodity / forex / crypto the theme moves
121
+ ```
122
+
123
+ ## Straight to pandas
124
+
125
+ `.to_df()` returns the **same columns as the [sample dataset](https://github.com/MarketIV/marketiv/tree/main/datasets)** (`impact-signals.csv`) — so
126
+ anything you prototyped on the free CSV runs unchanged on the live graph:
127
+
128
+ ```python
129
+ df = mdx.news(megatrend="ai-power", impact="indirect").to_df()
130
+ # published_at · title · brief_text · entity_name · entity_ticker · entity_type · direction ·
131
+ # aspect · reason · relevance · impact · impact_score · node_name · entity_country · publisher · url
132
+
133
+ df.groupby(["entity_type", "direction"]).size() # who the news lands on, +/− by asset class (the model's read)
134
+ df[df.aspect == "tariff"].entity_name.value_counts() # who the tariff channel touches
135
+ ```
136
+
137
+ ## Filtering — read this before you filter
138
+
139
+ **Feed filters are EVENT-level, not row-level.** `news(direction=…, aspect=…, news_type=…, country=…)`
140
+ selects **articles** that contain *at least one* matching impact and returns the **whole** article with
141
+ **all** its entities and aspects. So `direction="pos"` can return an article that also moves something `neg`,
142
+ and `aspect="supply"` can return one whose other entities are hit via `monetary`. For exact per-row
143
+ filtering, **post-filter the entities**:
144
+
145
+ ```python
146
+ for s in mdx.news(news_type="commodity_supply", direction="pos"):
147
+ for e in s.entities:
148
+ for a in (e.impact.aspects if e.impact else []):
149
+ if e.type == "commodity" and a.direction == "pos" and a.aspect == "supply":
150
+ ... # exact row you asked for
151
+ ```
152
+
153
+ **Two different "direct" axes** (don't conflate):
154
+ - `Signal.impact_type` (`direct`|`indirect`) = the **article's** relation to the **queried node** —
155
+ epicenter (`direct`) vs ripple (`indirect`). Set by `news(megatrend=…, impact="indirect")`.
156
+ - `Entity.direct` (`True`|`False`) = whether that **entity** is **factually mentioned** in the article
157
+ (`True`) vs **impact-only / not named** (`False`).
158
+
159
+ **Narrow the feed by entity — server-side.** `news()` (and `news_search()`) filter by
160
+ `entity_type` / `only_scored` / `min_relevance` in the API, so `page.total` stays the exact filtered
161
+ count (no wasted paging). These keep the *whole* article — for an exact per-row cut, still post-filter
162
+ the entities as above.
163
+
164
+ ```python
165
+ mdx.news(entity_type="commodity") # feed → only stories that move a commodity
166
+ mdx.news(entity_type="private") # only stories moving a private co (OpenAI, SpaceX)
167
+ mdx.news(only_scored=True) # drop mention-only articles (keep judged impact)
168
+ mdx.news(min_relevance=0.8) # only a strongly-relevant scored entity
169
+ mdx.news(megatrend="ai-power", entity_type="crypto") # entity filters compose with megatrend scope
170
+ mdx.news_search("oil supply shock", entity_type="commodity") # search supports entity_type too
171
+ mdx.news_by_tickers("NVDA.US") # a ticker's news (direct + indirect by default)
172
+ mdx.news_by_tickers("NVDA.US", link="direct") # only where NVDA is factually named (not ripple-only)
173
+ mdx.news_search("oil shock", megatrend="ai-power") # semantic search, scoped to a trend
174
+ mdx.megatrends("ai-power").off_coverage() # private / off-coverage roster
175
+ ```
176
+
177
+ > Entity filters do **not** apply to `impact="indirect"` (the ripple feed) — the API returns 400 if you
178
+ > combine them. On `news_by_tickers`, the ticker set already scopes the entities.
179
+
180
+ **Only entities with a *scored* impact** (many are mentioned-only) — `only_scored=True` narrows to such
181
+ articles server-side; then read the scored entities off each signal:
182
+
183
+ ```python
184
+ scored = [e for s in mdx.news(megatrend="ai-power", only_scored=True)
185
+ for e in s.entities if e.impact and e.impact.aspects]
186
+ ```
187
+
188
+ **What to expect in `entities` (not bugs):**
189
+ - **Mostly mention-only.** An article's `entities` mix two kinds: *mentioned* (`e.direct is True`,
190
+ `e.impact is None`) — every ticker the article names — and *scored* (`e.impact is not None`) — the ones
191
+ the model judged materially moved. Mentions usually **outnumber** scored (a story names many tickers but
192
+ moves a few). Want just the movers? `only_scored=True` or filter `e.impact`.
193
+ - **`entities` can be empty.** `include=entities` attaches entities *if the article maps to any*; a macro /
194
+ policy / commodity story that names no covered company (e.g. "China bans helium exports") legitimately
195
+ returns `entities == []`. It means *no entity resolved*, not a dropped/failed enrich.
196
+
197
+ **Provenance.** A scored `impact` ships its evidence, not just a label: `aspect`+`direction`+`relevance`
198
+ (the judgment), `reason` (why), `Entity.direct` (named vs affected-only), and `impact.label_version` — the
199
+ labeling-scheme version (currently `"1.0"`), a per-label stamp that bumps when the model/prompt/taxonomy
200
+ changes so you can detect and re-evaluate shifts. Audit or gate on it: `e.impact.label_version`.
201
+
202
+ **`stock(t).news()` is a stock-centric timeline** — a `StockNews` (the stock's own `impact` /`trend`/
203
+ `relevance`), **not** an entity graph (no `entities[]`). For the full graph of an article, use `news()`.
204
+
205
+ **Story-collapse (on by default).** The same story is often republished / rewritten across outlets.
206
+ Every news feed — `news()`, `news_search()`, `news_by_tickers()` and `stock(t).news()` — merges those
207
+ near-duplicates into a single signal by default (cosine-similarity grouping, server-side) so a feed
208
+ reads one-story-one-row. Pass `collapse=False` when you want the raw, un-deduped stream — e.g. to
209
+ measure coverage volume:
210
+
211
+ ```python
212
+ merged = mdx.news(megatrend="ai-power").to_list() # deduped (default)
213
+ raw = mdx.news(megatrend="ai-power", collapse=False).to_list() # every republication
214
+ ```
215
+
216
+ Each collapsed signal also carries its cluster metadata — group a dashboard by `story_id` and see reach +
217
+ lifespan without double-counting:
218
+
219
+ ```python
220
+ for s in mdx.news(megatrend="ai-power", limit=5):
221
+ print(s.story_id, s.dup_count, s.first_seen, s.latest_seen) # cluster id · outlets · broke · last echo
222
+ ```
223
+
224
+ ## The brief — the whole picture in one call
225
+
226
+ `mdx.theme(id).summary(...)` (a theme = a megatrend node; also `mdx.megatrends(id).summary(...)`) returns
227
+ a pre-composed **analyst brief** so you don't stitch 5+ requests together. It's a fixed composite `dict`,
228
+ not a paginated list:
229
+
230
+ ```python
231
+ brief = mdx.theme("ai-power").summary(window="30d") # 7d/30d/90d/180d/1y or mtd/qtd/ytd (or from_/to)
232
+ brief["pulse"] # story_count, net_direction, pos/neg share + a `series` (volume+sentiment/bucket)
233
+ brief["top_stories"] # epicenter, deduped; market-wraps & no-member-named stories deprioritized
234
+ brief["ripple"] # indirect (ripple-in) stories, each with `via`
235
+ brief["winners"], brief["losers"] # member stocks by net direction
236
+ brief["aspect_heatmap"] # which channels the theme is playing out through
237
+ brief["top_entities"] # operating companies most in the news
238
+ brief["top_assets"] # commodity / forex / crypto the theme moves (split out from companies)
239
+ ```
240
+
241
+ Every count is **story-deduped** (20 outlets on one story = 1). The `pulse.series` is the momentum signal —
242
+ there's no single momentum scalar (the latest bucket is the current, partial period). Cost: 15 credits.
243
+
244
+ **Any scope, not just a theme.** `mdx.brief(...)` composes the *same* object over any AND-combination of
245
+ **megatrend / news_type / country / aspect / gics** (≥1 required) — a theme, a market, a news category,
246
+ an impact channel, a GICS sector, or a mix:
247
+
248
+ ```python
249
+ mdx.brief(country="JP") # how is Japan doing right now?
250
+ mdx.brief(news_type="commodity_supply", country="JP") # commodity news, in Japan
251
+ mdx.brief(aspect="tariff", window="90d") # everything moving via tariffs
252
+ mdx.brief(megatrend="semiconductors", country="US") # a theme, narrowed to one market
253
+ ```
254
+
255
+ `applied_scope` echoes what you filtered. `node` + `ripple` appear **only when a single `megatrend`**
256
+ anchors the brief (without a theme there's no ripple). Under a megatrend scope `winners`/`losers` are the
257
+ theme's members; otherwise they're the top +/- companies in the scope. `theme(id).summary(...)` ==
258
+ `brief(megatrend=id, ...)`. `megatrend`/`news_type`/`country`/`aspect`/`gics` each take one value or a list.
259
+
260
+ ## Metering & errors
261
+
262
+ Every call carries `X-Credits-Charged` / `X-RateLimit-*`; check your balance any time (free, unmetered):
263
+
264
+ ```python
265
+ mdx.account() # {'plan': …, 'credits': {'balance', 'daily_quota', 'resets_at', 'unlimited'}, 'rate_limit': …}
266
+ ```
267
+
268
+ ```python
269
+ from marketiv.errors import QuotaError, RateLimitError
270
+
271
+ try:
272
+ signals = mdx.news(megatrend="ai-power").to_list()
273
+ except RateLimitError as e:
274
+ time.sleep(e.retry_after or 1)
275
+ except QuotaError:
276
+ ... # daily quota spent — resets 00:00 UTC
277
+ ```
278
+
279
+ Enum values (`aspect`, `direction`, `entity_type`, …) are **type hints** for your editor — the API is the
280
+ source of truth, so new values work without upgrading the SDK. The live list: `mdx.enums()`.
281
+
282
+ ## Reference
283
+
284
+ `news` · `news_search` · `news_types` · `megatrends` (`.stocks` / `.off_coverage`) · `gics` (`.stocks`) ·
285
+ `stocks` (search + screener) · `stock` (`.news` / `.competitors` / `.peers`) · `enums` · `account`.
286
+ Full API docs: [marketiv.app](https://marketiv.app).
287
+
288
+ ## License
289
+
290
+ MIT. Built by [MarketIV](https://marketiv.app) — *democratizing financial data.*
@@ -0,0 +1,261 @@
1
+ <h1 align="center">marketiv</h1>
2
+
3
+ <p align="center">
4
+ <b>The financial impact graph, in Python.</b><br>
5
+ News → who it <i>touches</i> and <i>why</i> — the causal channel, the story's <i>lean</i>, and the
6
+ <i>ripple</i> — across stocks, commodities, FX, crypto, <b>and private companies</b>.<br>
7
+ A research, screening &amp; feature layer — direction is the news's content lean, not a price forecast.
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://marketiv.app/playground"><b>▶ Live playground</b></a> ·
12
+ <a href="https://marketiv.app">Docs & pricing</a> ·
13
+ <a href="https://github.com/MarketIV/marketiv/tree/main/datasets">Sample dataset</a>
14
+ </p>
15
+
16
+ ---
17
+
18
+ ```bash
19
+ pip install marketiv # add [pandas] for .to_df(): pip install "marketiv[pandas]"
20
+ ```
21
+
22
+ **An API key is required.** Create a free one at **https://marketiv.app** (sign in → *API keys*), then
23
+ pass it to the client (keep it out of source control — read it from an env var / secret in real apps):
24
+
25
+ ```python
26
+ from marketiv import MarketIV
27
+
28
+ mdx = MarketIV(api_key="avn_live_…") # your key from https://marketiv.app
29
+ for s in mdx.news(megatrend="ai-power", impact="indirect"):
30
+ print(s.title, [(e.name, e.impact.net_direction) for e in s.entities])
31
+ ```
32
+
33
+ That's the whole graph: every news event, every affected entity, labeled with **direction**,
34
+ **relevance**, the causal **aspect** (the *why*), and whether it's the **epicenter** or a **ripple** —
35
+ across five asset classes, **including private companies** ticker feeds can't see.
36
+
37
+ > **No key yet?** Explore everything with zero signup in the [playground](https://marketiv.app/playground),
38
+ > then grab a free key at **[https://marketiv.app](https://marketiv.app)**. Every request is
39
+ > authenticated with your key (`Authorization: Bearer <key>`) and metered in credits.
40
+
41
+ ## Why the SDK (not just `requests`)
42
+
43
+ - **Typed graph** — `signal.entities[0].impact.aspects[0].direction` with autocomplete, not raw dicts.
44
+ - **Auto-pagination** — `for s in mdx.news(...)` pages for you. `limit=` caps how many you get
45
+ (default 50; `limit=None` walks the whole match set); `page.total` is the full count.
46
+ - **Names, not ids** — `megatrend="ai-power"` or `"AI Power & Cooling"` or `10040000` all work.
47
+ - **`.to_df()`** — the whole result as a pandas DataFrame, one row per *(event × entity × aspect)*.
48
+ - **Typed errors** — `AuthError`, `QuotaError`, `RateLimitError`, `BadRequestError`, `NotFoundError`.
49
+
50
+ ## The graph, a few ways
51
+
52
+ ```python
53
+ # 1. Ripple: a themed event that also touches NON-thematic entities (our differentiator)
54
+ for s in mdx.news(megatrend="semiconductors", impact="indirect", limit=50):
55
+ ...
56
+
57
+ # 2. Beyond tickers: private companies in a trend (OpenAI, Anthropic, Ampere, ChangXin…)
58
+ for c in mdx.megatrends("semiconductors").off_coverage():
59
+ print(c.name, c.type, "→", c.megatrend["node_name"])
60
+
61
+ # 2b. …or just the NEWS that moves private companies — one server-side filter on the feed
62
+ for s in mdx.news(entity_type="private", only_scored=True): # also: crypto / commodity / forex / stock
63
+ print(s.title, [e.name for e in s.entities if e.type == "private"])
64
+
65
+ # 3. Per-stock impact timeline + its news-derived rivals
66
+ tl = mdx.stock("NVDA.US").news(aspect="competition")
67
+ peers = mdx.stock("NVDA.US").competitors()
68
+
69
+ # 3b. Find ANY entity — stock, commodity, crypto, forex, or an off-coverage PRIVATE company —
70
+ # each hit carries a ready-to-use .ticker (+ .type) you feed straight back into by-tickers
71
+ gold = mdx.stocks(q="gold").first().ticker # -> "GOLD" (the commodity)
72
+ openai = mdx.stocks(q="openai").first().ticker # -> "oc:52" (private, off-coverage)
73
+ news = mdx.news_by_tickers(["NVDA.US", openai]) # NVIDIA + OpenAI news in one feed
74
+
75
+ # 4. News-driven screen — where the news leans positive on a theme (the model's read, for research)
76
+ positive_lean = mdx.stocks(megatrend="ai-power", direction="pos", country="US", order_by="news_count")
77
+
78
+ # 5. Semantic search — match news by MEANING, not keywords (each hit is explainable)
79
+ for s in mdx.news_search("chip export controls to China", limit=5):
80
+ print(round(s.similarity, 2), s.scored, s.title) # how-strongly-matched + does-it-move-an-entity
81
+
82
+ # 6. Brief in ONE call — pulse timeseries + top stories + winners/losers + heatmap + assets.
83
+ # Scope by theme, market, news-type, impact-channel, GICS sector, or any mix (>=1 scope).
84
+ brief = mdx.theme("ai-power").summary(window="qtd") # theme brief (== brief(megatrend="ai-power"))
85
+ brief = mdx.brief(news_type="commodity_supply", country="JP", window="90d") # "commodity news, in Japan"
86
+ brief = mdx.brief(gics="2550", country=["GB","DE","FR","IT","ES","NL"]) # "European retail pulse" (sector x region)
87
+ # Find a GICS code by name — mdx.gics() returns the whole bounded taxonomy (filter client-side):
88
+ hits = [(g.code, g.name) for g in mdx.gics() if "retail" in g.name.lower()] # -> ('2550','Consumer Discretionary Distribution & Retail'), …
89
+ print(brief["pulse"]["story_count"], brief["pulse"]["net_direction"])
90
+ print([w["ticker"] for w in brief["winners"]], "vs", [l["ticker"] for l in brief["losers"]])
91
+ print([a["name"] for a in brief["top_assets"]]) # commodity / forex / crypto the theme moves
92
+ ```
93
+
94
+ ## Straight to pandas
95
+
96
+ `.to_df()` returns the **same columns as the [sample dataset](https://github.com/MarketIV/marketiv/tree/main/datasets)** (`impact-signals.csv`) — so
97
+ anything you prototyped on the free CSV runs unchanged on the live graph:
98
+
99
+ ```python
100
+ df = mdx.news(megatrend="ai-power", impact="indirect").to_df()
101
+ # published_at · title · brief_text · entity_name · entity_ticker · entity_type · direction ·
102
+ # aspect · reason · relevance · impact · impact_score · node_name · entity_country · publisher · url
103
+
104
+ df.groupby(["entity_type", "direction"]).size() # who the news lands on, +/− by asset class (the model's read)
105
+ df[df.aspect == "tariff"].entity_name.value_counts() # who the tariff channel touches
106
+ ```
107
+
108
+ ## Filtering — read this before you filter
109
+
110
+ **Feed filters are EVENT-level, not row-level.** `news(direction=…, aspect=…, news_type=…, country=…)`
111
+ selects **articles** that contain *at least one* matching impact and returns the **whole** article with
112
+ **all** its entities and aspects. So `direction="pos"` can return an article that also moves something `neg`,
113
+ and `aspect="supply"` can return one whose other entities are hit via `monetary`. For exact per-row
114
+ filtering, **post-filter the entities**:
115
+
116
+ ```python
117
+ for s in mdx.news(news_type="commodity_supply", direction="pos"):
118
+ for e in s.entities:
119
+ for a in (e.impact.aspects if e.impact else []):
120
+ if e.type == "commodity" and a.direction == "pos" and a.aspect == "supply":
121
+ ... # exact row you asked for
122
+ ```
123
+
124
+ **Two different "direct" axes** (don't conflate):
125
+ - `Signal.impact_type` (`direct`|`indirect`) = the **article's** relation to the **queried node** —
126
+ epicenter (`direct`) vs ripple (`indirect`). Set by `news(megatrend=…, impact="indirect")`.
127
+ - `Entity.direct` (`True`|`False`) = whether that **entity** is **factually mentioned** in the article
128
+ (`True`) vs **impact-only / not named** (`False`).
129
+
130
+ **Narrow the feed by entity — server-side.** `news()` (and `news_search()`) filter by
131
+ `entity_type` / `only_scored` / `min_relevance` in the API, so `page.total` stays the exact filtered
132
+ count (no wasted paging). These keep the *whole* article — for an exact per-row cut, still post-filter
133
+ the entities as above.
134
+
135
+ ```python
136
+ mdx.news(entity_type="commodity") # feed → only stories that move a commodity
137
+ mdx.news(entity_type="private") # only stories moving a private co (OpenAI, SpaceX)
138
+ mdx.news(only_scored=True) # drop mention-only articles (keep judged impact)
139
+ mdx.news(min_relevance=0.8) # only a strongly-relevant scored entity
140
+ mdx.news(megatrend="ai-power", entity_type="crypto") # entity filters compose with megatrend scope
141
+ mdx.news_search("oil supply shock", entity_type="commodity") # search supports entity_type too
142
+ mdx.news_by_tickers("NVDA.US") # a ticker's news (direct + indirect by default)
143
+ mdx.news_by_tickers("NVDA.US", link="direct") # only where NVDA is factually named (not ripple-only)
144
+ mdx.news_search("oil shock", megatrend="ai-power") # semantic search, scoped to a trend
145
+ mdx.megatrends("ai-power").off_coverage() # private / off-coverage roster
146
+ ```
147
+
148
+ > Entity filters do **not** apply to `impact="indirect"` (the ripple feed) — the API returns 400 if you
149
+ > combine them. On `news_by_tickers`, the ticker set already scopes the entities.
150
+
151
+ **Only entities with a *scored* impact** (many are mentioned-only) — `only_scored=True` narrows to such
152
+ articles server-side; then read the scored entities off each signal:
153
+
154
+ ```python
155
+ scored = [e for s in mdx.news(megatrend="ai-power", only_scored=True)
156
+ for e in s.entities if e.impact and e.impact.aspects]
157
+ ```
158
+
159
+ **What to expect in `entities` (not bugs):**
160
+ - **Mostly mention-only.** An article's `entities` mix two kinds: *mentioned* (`e.direct is True`,
161
+ `e.impact is None`) — every ticker the article names — and *scored* (`e.impact is not None`) — the ones
162
+ the model judged materially moved. Mentions usually **outnumber** scored (a story names many tickers but
163
+ moves a few). Want just the movers? `only_scored=True` or filter `e.impact`.
164
+ - **`entities` can be empty.** `include=entities` attaches entities *if the article maps to any*; a macro /
165
+ policy / commodity story that names no covered company (e.g. "China bans helium exports") legitimately
166
+ returns `entities == []`. It means *no entity resolved*, not a dropped/failed enrich.
167
+
168
+ **Provenance.** A scored `impact` ships its evidence, not just a label: `aspect`+`direction`+`relevance`
169
+ (the judgment), `reason` (why), `Entity.direct` (named vs affected-only), and `impact.label_version` — the
170
+ labeling-scheme version (currently `"1.0"`), a per-label stamp that bumps when the model/prompt/taxonomy
171
+ changes so you can detect and re-evaluate shifts. Audit or gate on it: `e.impact.label_version`.
172
+
173
+ **`stock(t).news()` is a stock-centric timeline** — a `StockNews` (the stock's own `impact` /`trend`/
174
+ `relevance`), **not** an entity graph (no `entities[]`). For the full graph of an article, use `news()`.
175
+
176
+ **Story-collapse (on by default).** The same story is often republished / rewritten across outlets.
177
+ Every news feed — `news()`, `news_search()`, `news_by_tickers()` and `stock(t).news()` — merges those
178
+ near-duplicates into a single signal by default (cosine-similarity grouping, server-side) so a feed
179
+ reads one-story-one-row. Pass `collapse=False` when you want the raw, un-deduped stream — e.g. to
180
+ measure coverage volume:
181
+
182
+ ```python
183
+ merged = mdx.news(megatrend="ai-power").to_list() # deduped (default)
184
+ raw = mdx.news(megatrend="ai-power", collapse=False).to_list() # every republication
185
+ ```
186
+
187
+ Each collapsed signal also carries its cluster metadata — group a dashboard by `story_id` and see reach +
188
+ lifespan without double-counting:
189
+
190
+ ```python
191
+ for s in mdx.news(megatrend="ai-power", limit=5):
192
+ print(s.story_id, s.dup_count, s.first_seen, s.latest_seen) # cluster id · outlets · broke · last echo
193
+ ```
194
+
195
+ ## The brief — the whole picture in one call
196
+
197
+ `mdx.theme(id).summary(...)` (a theme = a megatrend node; also `mdx.megatrends(id).summary(...)`) returns
198
+ a pre-composed **analyst brief** so you don't stitch 5+ requests together. It's a fixed composite `dict`,
199
+ not a paginated list:
200
+
201
+ ```python
202
+ brief = mdx.theme("ai-power").summary(window="30d") # 7d/30d/90d/180d/1y or mtd/qtd/ytd (or from_/to)
203
+ brief["pulse"] # story_count, net_direction, pos/neg share + a `series` (volume+sentiment/bucket)
204
+ brief["top_stories"] # epicenter, deduped; market-wraps & no-member-named stories deprioritized
205
+ brief["ripple"] # indirect (ripple-in) stories, each with `via`
206
+ brief["winners"], brief["losers"] # member stocks by net direction
207
+ brief["aspect_heatmap"] # which channels the theme is playing out through
208
+ brief["top_entities"] # operating companies most in the news
209
+ brief["top_assets"] # commodity / forex / crypto the theme moves (split out from companies)
210
+ ```
211
+
212
+ Every count is **story-deduped** (20 outlets on one story = 1). The `pulse.series` is the momentum signal —
213
+ there's no single momentum scalar (the latest bucket is the current, partial period). Cost: 15 credits.
214
+
215
+ **Any scope, not just a theme.** `mdx.brief(...)` composes the *same* object over any AND-combination of
216
+ **megatrend / news_type / country / aspect / gics** (≥1 required) — a theme, a market, a news category,
217
+ an impact channel, a GICS sector, or a mix:
218
+
219
+ ```python
220
+ mdx.brief(country="JP") # how is Japan doing right now?
221
+ mdx.brief(news_type="commodity_supply", country="JP") # commodity news, in Japan
222
+ mdx.brief(aspect="tariff", window="90d") # everything moving via tariffs
223
+ mdx.brief(megatrend="semiconductors", country="US") # a theme, narrowed to one market
224
+ ```
225
+
226
+ `applied_scope` echoes what you filtered. `node` + `ripple` appear **only when a single `megatrend`**
227
+ anchors the brief (without a theme there's no ripple). Under a megatrend scope `winners`/`losers` are the
228
+ theme's members; otherwise they're the top +/- companies in the scope. `theme(id).summary(...)` ==
229
+ `brief(megatrend=id, ...)`. `megatrend`/`news_type`/`country`/`aspect`/`gics` each take one value or a list.
230
+
231
+ ## Metering & errors
232
+
233
+ Every call carries `X-Credits-Charged` / `X-RateLimit-*`; check your balance any time (free, unmetered):
234
+
235
+ ```python
236
+ mdx.account() # {'plan': …, 'credits': {'balance', 'daily_quota', 'resets_at', 'unlimited'}, 'rate_limit': …}
237
+ ```
238
+
239
+ ```python
240
+ from marketiv.errors import QuotaError, RateLimitError
241
+
242
+ try:
243
+ signals = mdx.news(megatrend="ai-power").to_list()
244
+ except RateLimitError as e:
245
+ time.sleep(e.retry_after or 1)
246
+ except QuotaError:
247
+ ... # daily quota spent — resets 00:00 UTC
248
+ ```
249
+
250
+ Enum values (`aspect`, `direction`, `entity_type`, …) are **type hints** for your editor — the API is the
251
+ source of truth, so new values work without upgrading the SDK. The live list: `mdx.enums()`.
252
+
253
+ ## Reference
254
+
255
+ `news` · `news_search` · `news_types` · `megatrends` (`.stocks` / `.off_coverage`) · `gics` (`.stocks`) ·
256
+ `stocks` (search + screener) · `stock` (`.news` / `.competitors` / `.peers`) · `enums` · `account`.
257
+ Full API docs: [marketiv.app](https://marketiv.app).
258
+
259
+ ## License
260
+
261
+ MIT. Built by [MarketIV](https://marketiv.app) — *democratizing financial data.*
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "marketiv"
7
+ version = "0.16.0"
8
+ description = "The MarketIV financial impact graph, in Python — news→market impact (why, who, direction, ripple) across stocks, commodities, FX, crypto, and private companies."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "MarketIV", email = "hello@marketdx.lab.ai" }]
13
+ keywords = ["finance", "news", "market-impact", "megatrends", "impact-graph", "quant", "api", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ ]
22
+ dependencies = ["httpx>=0.24"]
23
+
24
+ [project.optional-dependencies]
25
+ pandas = ["pandas>=1.3"]
26
+ dev = ["pandas>=1.3", "pytest>=7", "mypy>=1.0", "frictionless>=5"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://marketiv.app"
30
+ Playground = "https://marketiv.app/playground"
31
+ Repository = "https://github.com/MarketIV/marketiv"
32
+ "Sample dataset" = "https://github.com/MarketIV/marketiv/tree/main/datasets"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/marketiv"]