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.
- marketiv-0.16.0/.gitignore +16 -0
- marketiv-0.16.0/LICENSE +21 -0
- marketiv-0.16.0/PKG-INFO +290 -0
- marketiv-0.16.0/README.md +261 -0
- marketiv-0.16.0/pyproject.toml +35 -0
- marketiv-0.16.0/src/marketiv/__init__.py +57 -0
- marketiv-0.16.0/src/marketiv/_http.py +108 -0
- marketiv-0.16.0/src/marketiv/client.py +438 -0
- marketiv-0.16.0/src/marketiv/enums.py +48 -0
- marketiv-0.16.0/src/marketiv/errors.py +62 -0
- marketiv-0.16.0/src/marketiv/models.py +348 -0
- marketiv-0.16.0/src/marketiv/pagination.py +93 -0
- marketiv-0.16.0/src/marketiv/taxonomy.py +62 -0
marketiv-0.16.0/LICENSE
ADDED
|
@@ -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.
|
marketiv-0.16.0/PKG-INFO
ADDED
|
@@ -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 & 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 & 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"]
|