finzdata-huggingface 0.1.0__py3-none-any.whl

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,108 @@
1
+ """FinzData for Hugging Face: point-in-time market data, macro and backtests through function calling and MCP.
2
+
3
+ Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
4
+ """
5
+ import os
6
+
7
+ from ._core import (FinzDataMCP, FinzDataError, PROFILES, MCP_URL, __version__, ask as _ask,
8
+ to_openai_tools, mcp_server_config)
9
+
10
+ __all__ = ["ask", "openai_tools", "mcp_config", "FinzDataMCP", "FinzDataError", "PROFILES", "MCP_URL",
11
+ "DEFAULT_BASE_URL", "DEFAULT_MODEL", "__version__", "TOP_MODELS", "smolagents_tools", "smolagents_agent", "tiny_agent_config", "write_tiny_agent"]
12
+
13
+ DEFAULT_BASE_URL = "https://router.huggingface.co/v1"
14
+ TOP_MODELS = {
15
+ "qwen": "Qwen/Qwen3.8-2.4T-A95B",
16
+ "qwen-fast": "Qwen/Qwen3.8-27B",
17
+ "kimi": "moonshotai/Kimi-K3",
18
+ "glm": "zai-org/GLM-5.3",
19
+ "deepseek": "deepseek-ai/DeepSeek-V4-Pro-0813",
20
+ "deepseek-fast": "deepseek-ai/DeepSeek-V4.1-Flash",
21
+ "llama": "meta-llama/Llama-3.3-70B-Instruct",
22
+ "gpt-oss": "openai/gpt-oss-120b",
23
+ "gemma": "google/gemma-4-26B-A4B-it",
24
+ "minimax": "MiniMaxAI/MiniMax-M3",
25
+ }
26
+ DEFAULT_MODEL = TOP_MODELS["glm"]
27
+
28
+
29
+ def _base_url(base_url=None):
30
+ return base_url or DEFAULT_BASE_URL
31
+
32
+
33
+ def _model(model=None):
34
+ return model or os.environ.get("FINZDATA_HF_MODEL") or DEFAULT_MODEL
35
+
36
+
37
+ def _llm_key(llm_api_key=None):
38
+ return llm_api_key or os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
39
+
40
+
41
+ def openai_tools(profile="data", api_key=None):
42
+ """FinzData's tools in OpenAI function-calling format, fetched live from the FinzData MCP server."""
43
+ return to_openai_tools(FinzDataMCP(api_key=api_key).tools(profile))
44
+
45
+
46
+ def mcp_config(api_key=None):
47
+ """MCP server entry (``{"mcpServers": {"finzdata": ...}}``) for any MCP-capable agent, IDE or CLI."""
48
+ return mcp_server_config(api_key)
49
+
50
+
51
+ def ask(question, model=None, base_url=None, llm_api_key=None, finzdata_api_key=None, profile="data", **kw):
52
+ """Ask Hugging Face a market question; the model calls FinzData tools and returns its final answer."""
53
+ model = TOP_MODELS.get(model, model)
54
+ return _ask(question, base_url=_base_url(base_url), model=_model(model), llm_api_key=_llm_key(llm_api_key),
55
+ finzdata_api_key=finzdata_api_key, profile=profile, **kw)
56
+
57
+
58
+ def _mcp_params(api_key=None):
59
+ entry = mcp_server_config(api_key)["mcpServers"]["finzdata"]
60
+ params = {"url": MCP_URL, "transport": "streamable-http"}
61
+ if "headers" in entry:
62
+ params["headers"] = entry["headers"]
63
+ return params
64
+
65
+
66
+ def smolagents_tools(api_key=None, trust_remote_code=True):
67
+ """smolagents ToolCollection of FinzData tools (use as a context manager)."""
68
+ from smolagents import ToolCollection
69
+ return ToolCollection.from_mcp(_mcp_params(api_key), trust_remote_code=trust_remote_code)
70
+
71
+
72
+ def smolagents_agent(model_id=None, api_key=None):
73
+ """Context manager yielding a smolagents CodeAgent wired to FinzData and a Hub model."""
74
+ import contextlib
75
+
76
+ @contextlib.contextmanager
77
+ def _agent():
78
+ from smolagents import CodeAgent, InferenceClientModel
79
+ with smolagents_tools(api_key) as collection:
80
+ yield CodeAgent(tools=[*collection.tools], add_base_tools=False,
81
+ model=InferenceClientModel(model_id=TOP_MODELS.get(model_id, model_id) or DEFAULT_MODEL))
82
+ return _agent()
83
+
84
+
85
+ def tiny_agent_config(model=None, provider="auto", api_key=None):
86
+ """agent.json for ``tiny-agents run`` (huggingface_hub)."""
87
+ server = {"type": "http", "url": MCP_URL}
88
+ entry = mcp_server_config(api_key)["mcpServers"]["finzdata"]
89
+ if "headers" in entry:
90
+ server["headers"] = entry["headers"]
91
+ return {"model": TOP_MODELS.get(model, model) or DEFAULT_MODEL, "provider": provider, "servers": [server]}
92
+
93
+
94
+ def write_tiny_agent(directory="finzdata-agent", model=None, provider="auto", api_key=None, instructions=None):
95
+ """Write agent.json and PROMPT.md into ``directory``; run it with ``tiny-agents run <directory>``."""
96
+ import json
97
+ import pathlib
98
+ from ._core import SYSTEM_PROMPT_TAIL
99
+ d = pathlib.Path(directory)
100
+ d.mkdir(parents=True, exist_ok=True)
101
+ (d / "agent.json").write_text(json.dumps(tiny_agent_config(model, provider, api_key), indent=2))
102
+ if instructions is None:
103
+ try:
104
+ instructions = FinzDataMCP(api_key=api_key).instructions()[:4000]
105
+ except FinzDataError:
106
+ instructions = "You are a market research assistant with FinzData's point-in-time market data tools."
107
+ (d / "PROMPT.md").write_text(instructions + "\n\n" + SYSTEM_PROMPT_TAIL + "\n")
108
+ return d
@@ -0,0 +1,20 @@
1
+ import sys
2
+
3
+ from . import DEFAULT_MODEL, _base_url
4
+ from ._core import main as _main
5
+
6
+
7
+ def cli(argv=None):
8
+ argv = list(sys.argv[1:] if argv is None else argv)
9
+ if "--write-tiny-agent" in argv:
10
+ from . import write_tiny_agent
11
+ i = argv.index("--write-tiny-agent")
12
+ target = argv[i + 1] if len(argv) > i + 1 else "finzdata-agent"
13
+ print(f"Tiny Agent written to {write_tiny_agent(target)} - run: tiny-agents run {target}")
14
+ return 0
15
+ return _main(argv, defaults={"base_url": _base_url(), "model": DEFAULT_MODEL, "llm_key_env": "HF_TOKEN",
16
+ "model_env": "FINZDATA_HF_MODEL", "profile": "data", "prog": "finzdata-huggingface"})
17
+
18
+
19
+ if __name__ == "__main__":
20
+ sys.exit(cli())
@@ -0,0 +1,434 @@
1
+ """FinzData open-model bridge: lets any OpenAI-compatible chat model use FinzData's MCP tools.
2
+
3
+ Stdlib only. Shared verbatim by finzdata-qwen, finzdata-kimi, finzdata-glm, finzdata-deepseek,
4
+ finzdata-huggingface and finzdata-ollama.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import json
10
+ import os
11
+ import re
12
+ import ssl
13
+ import sys
14
+ import time
15
+ import urllib.error
16
+ import urllib.request
17
+
18
+ __version__ = "0.1.0"
19
+ MCP_URL = "https://api.finzdata.com/mcp"
20
+ USER_AGENT = f"finzdata-openmodels/{__version__}"
21
+
22
+ _LITE = ("search_datasets", "describe_dataset", "get_bars", "get_macro_vintage", "get_cot", "get_gex")
23
+ PROFILES = {
24
+ "lite": _LITE,
25
+ "data": _LITE + (
26
+ "get_universe", "get_fundamentals_pit", "get_quant_model", "get_13f", "get_events", "get_futures",
27
+ "get_futures_rolls", "get_crypto", "get_fx", "get_index", "get_global_pulse", "run_backtest",
28
+ "survivorship_audit", "compare_runs",
29
+ ),
30
+ "full": None,
31
+ }
32
+ WRITE_TOOLS = (
33
+ "publish_strategy", "fork_strategy", "star_strategy", "comment_strategy",
34
+ "register_strategy", "retire_strategy", "claim_mcp_server",
35
+ )
36
+ SYSTEM_PROMPT_TAIL = (
37
+ "Use the FinzData tools for any question about markets, prices, positioning, fundamentals, macro data or "
38
+ "backtests. When several markets match (for example several WTI crude contracts), use the main US benchmark "
39
+ "contract (NYMEX for WTI crude and natural gas, CME for E-mini equity index futures, COMEX for gold) unless the "
40
+ "user names another, and say which market you used. When the dataset is obvious (COT, bars, GEX, macro, 13F), call that data tool directly; use "
41
+ "search_datasets only when you are unsure which dataset fits. Call each tool once with the right arguments rather "
42
+ "than repeating it. Cite the dataset ids and snapshot ids the tools return. Never invent numbers: if a tool does not "
43
+ "return a figure, say so. When a result says data is beyond the free window, pass on the links it gives."
44
+ )
45
+ FREE_TIER_NOTE = (
46
+ "Access: this session uses FinzData's free AI preview - no API key needed. The preview covers the last 6 months of "
47
+ "data, so questions about recent months are fully answerable: fetch the data and answer with the real figures. "
48
+ "Never refuse, never use placeholders, and never ask the user to sign up or pay for data that is inside the preview. "
49
+ "Only when the user asks for something the preview does not cover (older than 6 months, or a tool says it is beyond "
50
+ "the free window) answer with what the preview has and say so plainly. "
51
+ "Close every answer with exactly one short line (once, even if a tool result also asks you to mention it): "
52
+ "'More data: a free FinzData API key (email only) at https://finzdata.com/account gives 12 months; full history at "
53
+ "https://finzdata.com/shop - then set FINZDATA_API_KEY.'"
54
+ )
55
+ KEYED_NOTE = ("Access: this session uses the user's FinzData API key, so their full plan applies. If a tool says data is "
56
+ "outside their plan, pass on the link it gives (https://finzdata.com/shop).")
57
+ FINAL_NUDGE = ("You have gathered enough. Do not call any more tools: answer the question now from the tool results "
58
+ "above, with the figures and the dataset ids.")
59
+ FREE_TIER_LINE = ("More data: a free FinzData API key (email only) at https://finzdata.com/account gives 12 months; "
60
+ "full history at https://finzdata.com/shop - then set FINZDATA_API_KEY.")
61
+ CLI_FREE_HINT = ("FinzData free AI preview (last 6 months, no key needed). More data: free API key at "
62
+ "https://finzdata.com/account (12 months) or full history at https://finzdata.com/shop - then set FINZDATA_API_KEY.")
63
+ EXAMPLES = (
64
+ "How are speculators positioned in crude oil futures by the latest COT report?",
65
+ "Show SPY's dealer gamma, call wall and put wall for the last 30 sessions.",
66
+ "What did the first-published CPI print say last month, before revisions?",
67
+ "Backtest a 12-month momentum strategy on US stocks, delisted names included.",
68
+ "How many ships transited the Strait of Hormuz each day last month?",
69
+ "Who are the largest 13F holders of NVDA, and who added the most last quarter?",
70
+ )
71
+ MAX_TOOL_CHARS = 12000
72
+ TOOL_BUDGET = 48000
73
+ TRIMMED_CHARS = 1500
74
+ RETRIES = 2
75
+ TIMEOUT_RETRIES = 1
76
+ RETRY_WAIT = 2.0
77
+ _ASSISTANT_KEYS = ("role", "content", "tool_calls")
78
+
79
+
80
+ class FinzDataError(RuntimeError):
81
+ """Raised for transport, HTTP and JSON-RPC failures."""
82
+
83
+
84
+ def api_key(explicit=None):
85
+ return explicit or os.environ.get("FINZDATA_API_KEY") or None
86
+
87
+
88
+ _SSL_CTX = None
89
+
90
+
91
+ def ssl_context():
92
+ """System trust store plus the certifi bundle (python.org builds on macOS ship with no root certificates)."""
93
+ global _SSL_CTX
94
+ if _SSL_CTX is None:
95
+ ctx = ssl.create_default_context()
96
+ try:
97
+ import certifi
98
+ ctx.load_verify_locations(certifi.where())
99
+ except Exception:
100
+ pass
101
+ _SSL_CTX = ctx
102
+ return _SSL_CTX
103
+
104
+
105
+ def _post(url, payload, headers, timeout):
106
+ req = urllib.request.Request(url, data=json.dumps(payload).encode(), method="POST", headers=headers)
107
+ try:
108
+ with urllib.request.urlopen(req, timeout=timeout, context=ssl_context() if url.startswith("https") else None) as r:
109
+ return r.status, dict(r.headers), r.read().decode("utf-8", "replace")
110
+ except urllib.error.HTTPError as e:
111
+ body = e.read().decode("utf-8", "replace") if e.fp else ""
112
+ raise FinzDataError(f"HTTP {e.code} from {url}: {body[:300]}") from None
113
+ except (urllib.error.URLError, TimeoutError, OSError) as e:
114
+ raise FinzDataError(f"cannot reach {url}: {e}") from None
115
+
116
+
117
+ def _parse_rpc(body, content_type, want_id):
118
+ if not body.strip():
119
+ return None
120
+ if "text/event-stream" in (content_type or ""):
121
+ for line in body.splitlines():
122
+ if line.startswith("data:"):
123
+ try:
124
+ msg = json.loads(line[5:].strip())
125
+ except ValueError:
126
+ continue
127
+ if isinstance(msg, dict) and msg.get("id") == want_id:
128
+ return msg
129
+ raise FinzDataError("no matching JSON-RPC message in event stream")
130
+ try:
131
+ return json.loads(body)
132
+ except ValueError:
133
+ raise FinzDataError(f"server returned non-JSON: {body[:300]}") from None
134
+
135
+
136
+ class FinzDataMCP:
137
+ """Minimal MCP client for FinzData (streamable HTTP, JSON or SSE responses)."""
138
+
139
+ def __init__(self, url=MCP_URL, api_key=None, timeout=60, allow_write=False):
140
+ self.url, self.timeout, self.allow_write = url, timeout, allow_write
141
+ self._key = globals()["api_key"](api_key)
142
+ self._id, self._session, self._init, self._tools = 0, None, None, None
143
+
144
+ def _headers(self):
145
+ h = {"Content-Type": "application/json", "Accept": "application/json, text/event-stream",
146
+ "User-Agent": USER_AGENT}
147
+ if self._key:
148
+ h["Authorization"] = f"Bearer {self._key}"
149
+ if self._session:
150
+ h["Mcp-Session-Id"] = self._session
151
+ return h
152
+
153
+ def _send(self, payload, want_id):
154
+ _, headers, body = _post(self.url, payload, self._headers(), self.timeout)
155
+ low = {k.lower(): v for k, v in headers.items()}
156
+ if low.get("mcp-session-id"):
157
+ self._session = low["mcp-session-id"]
158
+ return _parse_rpc(body, low.get("content-type", ""), want_id)
159
+
160
+ def _rpc(self, method, params=None):
161
+ self._id += 1
162
+ payload = {"jsonrpc": "2.0", "id": self._id, "method": method}
163
+ if params is not None:
164
+ payload["params"] = params
165
+ msg = self._send(payload, self._id)
166
+ if not isinstance(msg, dict):
167
+ raise FinzDataError(f"empty reply to {method}")
168
+ if msg.get("error"):
169
+ err = msg["error"]
170
+ raise FinzDataError(f"{method}: {err.get('message', err) if isinstance(err, dict) else err}")
171
+ return msg.get("result") or {}
172
+
173
+ def initialize(self):
174
+ if self._init is None:
175
+ self._init = self._rpc("initialize", {
176
+ "protocolVersion": "2025-06-18", "capabilities": {},
177
+ "clientInfo": {"name": "finzdata-openmodels", "version": __version__}})
178
+ try:
179
+ self._send({"jsonrpc": "2.0", "method": "notifications/initialized"}, None)
180
+ except FinzDataError:
181
+ pass
182
+ return self._init
183
+
184
+ def instructions(self):
185
+ return self.initialize().get("instructions") or ""
186
+
187
+ def list_tools(self):
188
+ if self._tools is None:
189
+ self.initialize()
190
+ self._tools = list(self._rpc("tools/list").get("tools") or [])
191
+ return self._tools
192
+
193
+ def tools(self, profile="data", allow_write=False, include=None):
194
+ allow = allow_write or self.allow_write
195
+ tools = self.list_tools()
196
+ if include is not None:
197
+ wanted = set(include)
198
+ elif profile not in PROFILES:
199
+ raise FinzDataError(f"unknown profile {profile!r}; use one of {sorted(PROFILES)}")
200
+ else:
201
+ wanted = None if PROFILES[profile] is None else set(PROFILES[profile])
202
+ out = []
203
+ for t in tools:
204
+ n = t.get("name")
205
+ if n in WRITE_TOOLS and not allow:
206
+ continue
207
+ if wanted is None or n in wanted:
208
+ out.append(t)
209
+ return out
210
+
211
+ def call(self, name, arguments=None):
212
+ if name in WRITE_TOOLS and not self.allow_write:
213
+ raise FinzDataError(f"{name} changes data on FinzData; build the client with allow_write=True to use it")
214
+ self.initialize()
215
+ res = self._rpc("tools/call", {"name": name, "arguments": arguments or {}})
216
+ text = "\n".join(c.get("text", "") for c in res.get("content") or [] if c.get("type") == "text")
217
+ return ("ERROR: " + text) if res.get("isError") else text
218
+
219
+
220
+ def to_openai_tools(mcp_tools):
221
+ out = []
222
+ for t in mcp_tools:
223
+ params = dict(t.get("inputSchema") or {"type": "object", "properties": {}})
224
+ for k in ("$schema", "title", "additionalProperties"):
225
+ params.pop(k, None)
226
+ params.setdefault("type", "object")
227
+ params.setdefault("properties", {})
228
+ out.append({"type": "function", "function": {
229
+ "name": t["name"], "description": (t.get("description") or t.get("title") or "")[:1024],
230
+ "parameters": params}})
231
+ return out
232
+
233
+
234
+ def chat_completion(base_url, model, messages, tools=None, api_key=None, timeout=150, extra=None):
235
+ body = {"model": model, "messages": messages}
236
+ if tools:
237
+ body["tools"], body["tool_choice"] = tools, "auto"
238
+ body.update(extra or {})
239
+ h = {"Content-Type": "application/json", "User-Agent": USER_AGENT}
240
+ if api_key:
241
+ h["Authorization"] = f"Bearer {api_key}"
242
+ url = base_url.rstrip("/") + "/chat/completions"
243
+ for attempt in range(RETRIES + 1):
244
+ try:
245
+ _, _, text = _post(url, body, h, timeout)
246
+ break
247
+ except FinzDataError as e:
248
+ timed_out = "timed out" in str(e)
249
+ transient = any(f"HTTP {c}" in str(e) for c in (429, 500, 502, 503, 504)) or timed_out
250
+ if attempt >= (TIMEOUT_RETRIES if timed_out else RETRIES) or not transient:
251
+ raise
252
+ time.sleep(RETRY_WAIT * (attempt + 1))
253
+ try:
254
+ return json.loads(text)
255
+ except ValueError:
256
+ raise FinzDataError(f"model server returned non-JSON: {text[:300]}") from None
257
+
258
+
259
+ _TAG_CALL = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.S)
260
+ # XML-style calls some models print as text, e.g. <invoke name="get_cot"><parameter name="market">..</parameter></invoke>
261
+ _XML_INVOKE = re.compile(r"<(?:[\w-]+:)?invoke\s+name=\"([\w.-]+)\"\s*>(.*?)</(?:[\w-]+:)?invoke>", re.S)
262
+ _XML_PARAM = re.compile(r"<(?:[\w-]+:)?parameter\s+name=\"([\w.-]+)\"\s*>(.*?)</(?:[\w-]+:)?parameter>", re.S)
263
+ _CALL_MARKUP = re.compile(r"<(?:[\w-]+:)?function_calls>.*?</(?:[\w-]+:)?function_calls>|<tool_call>.*?</tool_call>|"
264
+ r"<(?:[\w-]+:)?invoke\s.*?</(?:[\w-]+:)?invoke>|to=[\w.]+<\|message\|>", re.S)
265
+
266
+
267
+ def _text_calls(content):
268
+ """Tool calls a model wrote into its text instead of the tool_calls field (Hermes/Qwen JSON or XML invoke)."""
269
+ out = []
270
+ for m in _TAG_CALL.finditer(content or ""):
271
+ try:
272
+ call = json.loads(m.group(1))
273
+ out.append((call.get("name", ""), call.get("arguments") or {}))
274
+ except ValueError:
275
+ pass
276
+ for m in _XML_INVOKE.finditer(content or ""):
277
+ args = {}
278
+ for k, v in _XML_PARAM.findall(m.group(2)):
279
+ v = v.strip()
280
+ try:
281
+ args[k] = json.loads(v) if v[:1] in "[{" or v in ("true", "false") or re.fullmatch(r"-?\d+(\.\d+)?", v) else v
282
+ except ValueError:
283
+ args[k] = v
284
+ out.append((m.group(1), args))
285
+ return out
286
+
287
+
288
+ _REMINDER_LINE = re.compile(r"^.*finzdata\.com/(account|shop).*$", re.M)
289
+
290
+
291
+ def finish_answer(text, keyed):
292
+ """Exactly one free-tier reminder line at the end when there is no key; never one with a key."""
293
+ text = _CALL_MARKUP.sub("", text or "").strip()
294
+ if keyed:
295
+ return text
296
+ lines = [ln for ln in text.splitlines() if not (_REMINDER_LINE.match(ln) and len(ln) < 400)]
297
+ body = "\n".join(lines).rstrip(" -*\n")
298
+ return (body + "\n\n" + FREE_TIER_LINE).strip()
299
+
300
+
301
+ def _parse_args(raw):
302
+ if isinstance(raw, dict):
303
+ return raw, None
304
+ if raw in (None, ""):
305
+ return {}, None
306
+ try:
307
+ val = json.loads(raw)
308
+ return (val if isinstance(val, dict) else {}), None
309
+ except (ValueError, TypeError):
310
+ return None, "ERROR: arguments were not valid JSON"
311
+
312
+
313
+ def ask(question, base_url, model, llm_api_key=None, finzdata_api_key=None, profile="data", max_turns=8,
314
+ system=None, client=None, extra=None, on_tool=None):
315
+ """Answer `question` with `model`, letting it call FinzData tools. Returns the final text."""
316
+ mcp = client or FinzDataMCP(api_key=finzdata_api_key)
317
+ tools = to_openai_tools(mcp.tools(profile))
318
+ names = {t["function"]["name"] for t in tools}
319
+ keyed = bool(mcp._key)
320
+ access = KEYED_NOTE if keyed else FREE_TIER_NOTE
321
+ sys_msg = (system or mcp.instructions()) + "\n\n" + SYSTEM_PROMPT_TAIL + "\n\n" + access
322
+ messages = [{"role": "system", "content": sys_msg}, {"role": "user", "content": question}]
323
+
324
+ def run(name, raw):
325
+ args, err = _parse_args(raw)
326
+ if err:
327
+ result = err
328
+ elif name not in names:
329
+ result = f"ERROR: unknown tool {name}"
330
+ else:
331
+ try:
332
+ result = mcp.call(name, args)
333
+ except FinzDataError as e:
334
+ result = f"ERROR: {e}"
335
+ if on_tool:
336
+ on_tool(name, args, result)
337
+ if len(result) > MAX_TOOL_CHARS:
338
+ result = result[:MAX_TOOL_CHARS] + "\n[truncated: ask for a narrower date range or fewer markets for the rest]"
339
+ return result
340
+
341
+ def trim_context():
342
+ """Keep long tool sessions fast: once tool output passes TOOL_BUDGET, shorten the oldest results."""
343
+ tool_msgs = [m for m in messages if m.get("role") == "tool"]
344
+ total = sum(len(m["content"]) for m in tool_msgs)
345
+ for m in tool_msgs[:-1]:
346
+ if total <= TOOL_BUDGET:
347
+ break
348
+ if len(m["content"]) > TRIMMED_CHARS:
349
+ total -= len(m["content"]) - TRIMMED_CHARS
350
+ m["content"] = m["content"][:TRIMMED_CHARS] + "\n[earlier result trimmed to save context]"
351
+
352
+ last, minimal = "", False
353
+
354
+ def slim(msgs):
355
+ return [({k: v for k, v in m.items() if k in _ASSISTANT_KEYS} if m.get("role") == "assistant" else m) for m in msgs]
356
+
357
+ for turn in range(max_turns):
358
+ final = turn == max_turns - 1
359
+ if final: # last round: no more tools, answer from what has been gathered
360
+ messages.append({"role": "user", "content": FINAL_NUDGE})
361
+ ex = dict(extra or {}, tool_choice="none") if final else extra
362
+ try:
363
+ resp = chat_completion(base_url, model, slim(messages) if minimal else messages, tools, llm_api_key, extra=ex)
364
+ except FinzDataError as e:
365
+ if minimal or "HTTP 400" not in str(e) or not any(m.get("role") == "assistant" for m in messages):
366
+ raise
367
+ minimal = True # this server rejects extra assistant fields (e.g. reasoning); resend without them
368
+ resp = chat_completion(base_url, model, slim(messages), tools, llm_api_key, extra=ex)
369
+ try:
370
+ msg = resp["choices"][0]["message"]
371
+ except (KeyError, IndexError, TypeError):
372
+ raise FinzDataError(f"unexpected model reply: {json.dumps(resp)[:300]}") from None
373
+ content = msg.get("content") or ""
374
+ last = content or last
375
+ calls = msg.get("tool_calls") or []
376
+ if final:
377
+ return finish_answer(_CALL_MARKUP.sub("", content).strip() or last or f"Stopped after {max_turns} tool rounds.", keyed)
378
+ if calls:
379
+ kept = {k: v for k, v in msg.items() if v is not None or k == "content"}
380
+ kept["role"] = "assistant"
381
+ messages.append(kept) # unchanged, incl. reasoning_content: thinking models (Kimi K3, DeepSeek V4) need it
382
+ for c in calls:
383
+ fn = c.get("function") or {}
384
+ result = run(fn.get("name", ""), fn.get("arguments"))
385
+ messages.append({"role": "tool", "tool_call_id": c.get("id", ""), "name": fn.get("name", ""),
386
+ "content": result})
387
+ trim_context()
388
+ continue
389
+ text_calls = _text_calls(content)
390
+ if text_calls:
391
+ results = [f"<tool_response name=\"{n}\">\n{run(n, a)}\n</tool_response>" for n, a in text_calls]
392
+ messages.append({"role": "assistant", "content": content})
393
+ messages.append({"role": "user", "content": "\n".join(results)})
394
+ continue
395
+ return finish_answer(content, keyed)
396
+ return finish_answer(last or f"Stopped after {max_turns} tool rounds.", keyed)
397
+
398
+
399
+ def mcp_server_config(api_key=None, name="finzdata"):
400
+ entry = {"url": MCP_URL, "type": "streamable-http"}
401
+ key = globals()["api_key"](api_key)
402
+ if key:
403
+ entry["headers"] = {"Authorization": f"Bearer {key}"}
404
+ return {"mcpServers": {name: entry}}
405
+
406
+
407
+ def main(argv=None, defaults=None):
408
+ d = {"base_url": "http://localhost:11434/v1", "model": "qwen3:8b", "llm_key_env": "", "model_env": "",
409
+ "profile": "data", "prog": "finzdata"}
410
+ d.update(defaults or {})
411
+ p = argparse.ArgumentParser(prog=d["prog"], description="Ask an AI model a market question using FinzData data.")
412
+ p.add_argument("question", nargs="?", default="")
413
+ p.add_argument("--model", default=(os.environ.get(d["model_env"]) if d["model_env"] else None) or d["model"])
414
+ p.add_argument("--base-url", default=d["base_url"])
415
+ p.add_argument("--profile", default=d["profile"], choices=sorted(PROFILES))
416
+ p.add_argument("--show-tools", action="store_true", help="list the FinzData tools the model will see")
417
+ a = p.parse_args(argv)
418
+ try:
419
+ if a.show_tools:
420
+ for t in FinzDataMCP().tools(a.profile):
421
+ print(t["name"])
422
+ return 0
423
+ if not a.question:
424
+ p.error("a question is required")
425
+ key = os.environ.get(d["llm_key_env"]) if d["llm_key_env"] else None
426
+ answer = ask(a.question, a.base_url, a.model, llm_api_key=key, profile=a.profile,
427
+ on_tool=lambda n, args, r: print(f"-> {n}({json.dumps(args)})", file=sys.stderr))
428
+ print(answer)
429
+ if not api_key():
430
+ print("\n" + CLI_FREE_HINT, file=sys.stderr)
431
+ return 0
432
+ except FinzDataError as e:
433
+ print(f"error: {e}", file=sys.stderr)
434
+ return 1
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: finzdata-huggingface
3
+ Version: 0.1.0
4
+ Summary: FinzData for Hugging Face: point-in-time market data, macro and backtests through function calling and MCP.
5
+ Author-email: FinzData <info@finzdata.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://finzdata.com/agents
8
+ Project-URL: Documentation, https://finzdata.com/docs
9
+ Project-URL: Source, https://github.com/FinzData/finzdata-huggingface
10
+ Keywords: huggingface,smolagents,tiny-agents,inference-providers,qwen,kimi,glm,llama,gemma,mistral,gpt-oss,finzdata,mcp,market-data,function-calling,backtesting,ai-agents
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: certifi
21
+ Provides-Extra: smolagents
22
+ Requires-Dist: smolagents[mcp]; extra == "smolagents"
23
+ Provides-Extra: hub
24
+ Requires-Dist: huggingface_hub[mcp]; extra == "hub"
25
+ Dynamic: license-file
26
+
27
+ # FinzData for Hugging Face
28
+
29
+ Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
30
+
31
+ FinzData gives Hugging Face point-in-time market data, macro vintages and server-side backtests through function calling and MCP —
32
+ so the model answers market questions with real numbers it can cite, never guesses.
33
+
34
+ ## No API key needed
35
+
36
+ FinzData works the moment you install it: no FinzData account and no FinzData key. Your model gets the free AI preview
37
+ (the last 6 months of data) automatically, and it knows to tell you when older or deeper data needs a key.
38
+ When you want more, get a free API key (email only, 12 months of data) at https://finzdata.com/account, or full
39
+ history at https://finzdata.com/shop, then set `FINZDATA_API_KEY`. Nothing else changes.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pip install finzdata-huggingface
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from finzdata_huggingface import ask
51
+ print(ask("How did managed money positioning in crude oil change over the last 3 months?")) # no FinzData key needed
52
+ ```
53
+
54
+ ## What to ask
55
+
56
+ - "How are speculators positioned in crude oil futures by the latest COT report?"
57
+ - "Show SPY's dealer gamma, call wall and put wall for the last 30 sessions."
58
+ - "What did the first-published CPI print say last month, before revisions?"
59
+ - "Backtest a 12-month momentum strategy on US stocks, delisted names included."
60
+ - "How many ships transited the Strait of Hormuz each day last month?"
61
+ - "Who are the largest 13F holders of NVDA, and who added the most last quarter?"
62
+
63
+ ## Command line
64
+
65
+ ```bash
66
+ finzdata-huggingface "How did managed money positioning in crude oil change over the last 3 months?"
67
+ finzdata-huggingface --show-tools
68
+ ```
69
+
70
+ ## Inference Providers
71
+
72
+ Set `HF_TOKEN` and pick any of the top open models by family name or Hub id:
73
+
74
+ | family | Hub id |
75
+ |---|---|
76
+ | qwen | `Qwen/Qwen3.8-2.4T-A95B` |
77
+ | qwen-fast | `Qwen/Qwen3.8-27B` |
78
+ | kimi | `moonshotai/Kimi-K3` |
79
+ | glm | `zai-org/GLM-5.3` |
80
+ | deepseek | `deepseek-ai/DeepSeek-V4-Pro-0813` |
81
+ | deepseek-fast | `deepseek-ai/DeepSeek-V4.1-Flash` |
82
+ | llama | `meta-llama/Llama-3.3-70B-Instruct` |
83
+ | gpt-oss | `openai/gpt-oss-120b` |
84
+ | gemma | `google/gemma-4-26B-A4B-it` |
85
+ | minimax | `MiniMaxAI/MiniMax-M3` |
86
+
87
+ ```python
88
+ from finzdata_huggingface import ask
89
+ ask("Which ETFs saw the largest 13F ownership changes last quarter?", model="kimi")
90
+ ask("Plot-ready: SPX gamma flip for the last 20 sessions", model="meta-llama/Llama-3.3-70B-Instruct")
91
+ ```
92
+
93
+ Each id above was live with several Inference Providers in November 2026. Hub ids evolve; any Hub model with tool calling works.
94
+
95
+ ## smolagents
96
+
97
+ ```python
98
+ from finzdata_huggingface import smolagents_agent
99
+ with smolagents_agent("qwen") as agent:
100
+ agent.run("Backtest a 50/200-day moving-average cross on SPY since 2005 and report the Sharpe ratio.")
101
+ ```
102
+
103
+ Or take the tools alone: `with smolagents_tools() as tc: tools = tc.tools`.
104
+
105
+ ## Tiny Agents
106
+
107
+ ```bash
108
+ finzdata-huggingface --write-tiny-agent finzdata-agent
109
+ tiny-agents run ./finzdata-agent
110
+ ```
111
+
112
+ ## Hugging Face Spaces and Gradio
113
+
114
+ Try it in the browser: https://huggingface.co/spaces/FinzData/finzdata-open-models. The same `ask` call powers a Gradio app in a few lines.
115
+
116
+ ## Llama, Gemma, Mistral, gpt-oss and MiniMax
117
+
118
+ All of them run through the same `ask(..., model=...)`. Mistral's open models run best on your own machine: see `pip install finzdata-ollama`.
119
+
120
+ ## Use with any OpenAI-compatible server
121
+
122
+ ```python
123
+ from finzdata_huggingface import openai_tools, FinzDataMCP
124
+ tools = openai_tools() # pass as `tools=` to any chat.completions call
125
+ result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
126
+ ```
127
+
128
+ ## Tool profiles
129
+
130
+ `lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
131
+ Tools that change data on FinzData are never exposed unless you ask for them explicitly.
132
+
133
+ ## Coverage
134
+
135
+ 16,000+ US stocks that have traded on NYSE and Nasdaq since January 2000, delisted included, point in time; 5,100+ ETFs; 139 futures markets with every contract since 2007; point-in-time options gamma (GEX) for single stocks, ETFs and the major indexes; CFTC COT since 1986; 13F holdings; SEC fundamentals as filed; end-of-day options chains; 22,700+ macro series with every vintage kept; 512 crypto pairs (Bitcoin since 2013); FX for 79 currency pairs; 123 indexes incl. the VIX term structure; tick trades for 1,100+ US stocks and ETFs; FinzData Global Macro Pulse (ship transits through 28 chokepoints, port calls, flights, toll traffic); 1, 5, 15, 30-minute, hourly and daily bars; server-side backtests.
136
+
137
+ ## Access
138
+
139
+ No key needed to start: AI/MCP use gives the last 6 months. A free account (email only, https://finzdata.com/account) gives the last 12 months of daily US stock bars, COT, SEC fundamentals, US macro and 13F, and the last 30 days of futures and daily GEX. Full history: https://finzdata.com/shop — $19 one ticker, $49 one futures market, $195 for 5 years of the whole US market.
140
+
141
+ With a key:
142
+
143
+ ```bash
144
+ export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
145
+ ```
146
+
147
+ or pass `finzdata_api_key="fz_..."` to `ask`.
148
+
149
+ ## Links
150
+
151
+ - Site: https://finzdata.com
152
+ - Docs: https://finzdata.com/docs
153
+ - MCP server: https://api.finzdata.com/mcp
154
+ - Python client: `pip install finzdata`
155
+ - Contact: the FinzData team, info@finzdata.com
@@ -0,0 +1,9 @@
1
+ finzdata_huggingface/__init__.py,sha256=0-T39bPy9rSTVhspTuxhOBjZI0O6YhHb1c1cdyWQFhE,4664
2
+ finzdata_huggingface/__main__.py,sha256=cjveS9GtmrhL_LZQrw4KeLrPzsghkVNnpwpLNOBY1b8,748
3
+ finzdata_huggingface/_core.py,sha256=y3ZdqtwjhbaVIj0CTgHarOgI-ZIpKU27l3l_xclE-i4,19868
4
+ finzdata_huggingface-0.1.0.dist-info/licenses/LICENSE,sha256=vsxVRtneGOt7Hno_gT97_iH2M3tsteCip75_rUnHGi8,1065
5
+ finzdata_huggingface-0.1.0.dist-info/METADATA,sha256=L5oSjdE5XR-go2H13dclntqn02qpV_YcTBbZjYRuM9g,6595
6
+ finzdata_huggingface-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
7
+ finzdata_huggingface-0.1.0.dist-info/entry_points.txt,sha256=4ukxnj3yQ6QjXGblNwDTLZbvYsWGXWjmHnqzKPwLyJ8,75
8
+ finzdata_huggingface-0.1.0.dist-info/top_level.txt,sha256=DU6t96ZyCxR7UTPS_MWsPRkDMGdtCTOB44ahtojzYlg,21
9
+ finzdata_huggingface-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finzdata-huggingface = finzdata_huggingface.__main__:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FinzData
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 @@
1
+ finzdata_huggingface