finzdata-kimi 0.1.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,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,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: finzdata-kimi
3
+ Version: 0.1.0
4
+ Summary: FinzData for Kimi: 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-kimi
10
+ Keywords: kimi,kimi-k3,moonshot,kimi-cli,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
+ Dynamic: license-file
22
+
23
+ # FinzData for Kimi
24
+
25
+ Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
26
+
27
+ FinzData gives Kimi point-in-time market data, macro vintages and server-side backtests through function calling and MCP —
28
+ so the model answers market questions with real numbers it can cite, never guesses.
29
+
30
+ ## No API key needed
31
+
32
+ FinzData works the moment you install it: no FinzData account and no FinzData key. Your model gets the free AI preview
33
+ (the last 6 months of data) automatically, and it knows to tell you when older or deeper data needs a key.
34
+ When you want more, get a free API key (email only, 12 months of data) at https://finzdata.com/account, or full
35
+ history at https://finzdata.com/shop, then set `FINZDATA_API_KEY`. Nothing else changes.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install finzdata-kimi
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from finzdata_kimi import ask
47
+ print(ask("How did managed money positioning in crude oil change over the last 3 months?")) # no FinzData key needed
48
+ ```
49
+
50
+ ## What to ask
51
+
52
+ - "How are speculators positioned in crude oil futures by the latest COT report?"
53
+ - "Show SPY's dealer gamma, call wall and put wall for the last 30 sessions."
54
+ - "What did the first-published CPI print say last month, before revisions?"
55
+ - "Backtest a 12-month momentum strategy on US stocks, delisted names included."
56
+ - "How many ships transited the Strait of Hormuz each day last month?"
57
+ - "Who are the largest 13F holders of NVDA, and who added the most last quarter?"
58
+
59
+ ## Command line
60
+
61
+ ```bash
62
+ finzdata-kimi "How did managed money positioning in crude oil change over the last 3 months?"
63
+ finzdata-kimi --show-tools
64
+ ```
65
+
66
+ ## Moonshot API
67
+
68
+ Set `MOONSHOT_API_KEY` and ask. The default model is `kimi-k3`, which always thinks before it answers; FinzData passes its reasoning back unchanged between tool calls, as Kimi requires. Override with `model=` or `FINZDATA_KIMI_MODEL`.
69
+ Mainland China accounts: set `FINZDATA_KIMI_REGION=cn` to use `https://api.moonshot.cn/v1`.
70
+
71
+ ## Kimi CLI and Kimi Code
72
+
73
+ One command adds FinzData to the Kimi CLI's MCP config (`~/.kimi/mcp.json`), keeping every server already there:
74
+
75
+ ```bash
76
+ finzdata-kimi --install-cli-config
77
+ ```
78
+
79
+ Or add it by hand:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "finzdata": {
85
+ "url": "https://api.finzdata.com/mcp",
86
+ "type": "streamable-http",
87
+ "transport": "http"
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Any agent that accepts an MCP server URL can use `https://api.finzdata.com/mcp` the same way.
94
+
95
+ ## Kimi open weights
96
+
97
+ The Kimi models on Hugging Face (`moonshotai/Kimi-K3` and family) work through `pip install finzdata-huggingface` (Inference Providers) or any
98
+ OpenAI-compatible server you host (vLLM with tool calling enabled and the Kimi tool-call parser) using `ask(..., base_url="http://localhost:8000/v1")`.
99
+
100
+ ## Use with any OpenAI-compatible server
101
+
102
+ ```python
103
+ from finzdata_kimi import openai_tools, FinzDataMCP
104
+ tools = openai_tools() # pass as `tools=` to any chat.completions call
105
+ result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
106
+ ```
107
+
108
+ ## Tool profiles
109
+
110
+ `lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
111
+ Tools that change data on FinzData are never exposed unless you ask for them explicitly.
112
+
113
+ ## Coverage
114
+
115
+ 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.
116
+
117
+ ## Access
118
+
119
+ 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.
120
+
121
+ With a key:
122
+
123
+ ```bash
124
+ export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
125
+ ```
126
+
127
+ or pass `finzdata_api_key="fz_..."` to `ask`.
128
+
129
+ ## Links
130
+
131
+ - Site: https://finzdata.com
132
+ - Docs: https://finzdata.com/docs
133
+ - MCP server: https://api.finzdata.com/mcp
134
+ - Python client: `pip install finzdata`
135
+ - Contact: the FinzData team, info@finzdata.com
@@ -0,0 +1,113 @@
1
+ # FinzData for Kimi
2
+
3
+ Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
4
+
5
+ FinzData gives Kimi point-in-time market data, macro vintages and server-side backtests through function calling and MCP —
6
+ so the model answers market questions with real numbers it can cite, never guesses.
7
+
8
+ ## No API key needed
9
+
10
+ FinzData works the moment you install it: no FinzData account and no FinzData key. Your model gets the free AI preview
11
+ (the last 6 months of data) automatically, and it knows to tell you when older or deeper data needs a key.
12
+ When you want more, get a free API key (email only, 12 months of data) at https://finzdata.com/account, or full
13
+ history at https://finzdata.com/shop, then set `FINZDATA_API_KEY`. Nothing else changes.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install finzdata-kimi
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```python
24
+ from finzdata_kimi import ask
25
+ print(ask("How did managed money positioning in crude oil change over the last 3 months?")) # no FinzData key needed
26
+ ```
27
+
28
+ ## What to ask
29
+
30
+ - "How are speculators positioned in crude oil futures by the latest COT report?"
31
+ - "Show SPY's dealer gamma, call wall and put wall for the last 30 sessions."
32
+ - "What did the first-published CPI print say last month, before revisions?"
33
+ - "Backtest a 12-month momentum strategy on US stocks, delisted names included."
34
+ - "How many ships transited the Strait of Hormuz each day last month?"
35
+ - "Who are the largest 13F holders of NVDA, and who added the most last quarter?"
36
+
37
+ ## Command line
38
+
39
+ ```bash
40
+ finzdata-kimi "How did managed money positioning in crude oil change over the last 3 months?"
41
+ finzdata-kimi --show-tools
42
+ ```
43
+
44
+ ## Moonshot API
45
+
46
+ Set `MOONSHOT_API_KEY` and ask. The default model is `kimi-k3`, which always thinks before it answers; FinzData passes its reasoning back unchanged between tool calls, as Kimi requires. Override with `model=` or `FINZDATA_KIMI_MODEL`.
47
+ Mainland China accounts: set `FINZDATA_KIMI_REGION=cn` to use `https://api.moonshot.cn/v1`.
48
+
49
+ ## Kimi CLI and Kimi Code
50
+
51
+ One command adds FinzData to the Kimi CLI's MCP config (`~/.kimi/mcp.json`), keeping every server already there:
52
+
53
+ ```bash
54
+ finzdata-kimi --install-cli-config
55
+ ```
56
+
57
+ Or add it by hand:
58
+
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "finzdata": {
63
+ "url": "https://api.finzdata.com/mcp",
64
+ "type": "streamable-http",
65
+ "transport": "http"
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ Any agent that accepts an MCP server URL can use `https://api.finzdata.com/mcp` the same way.
72
+
73
+ ## Kimi open weights
74
+
75
+ The Kimi models on Hugging Face (`moonshotai/Kimi-K3` and family) work through `pip install finzdata-huggingface` (Inference Providers) or any
76
+ OpenAI-compatible server you host (vLLM with tool calling enabled and the Kimi tool-call parser) using `ask(..., base_url="http://localhost:8000/v1")`.
77
+
78
+ ## Use with any OpenAI-compatible server
79
+
80
+ ```python
81
+ from finzdata_kimi import openai_tools, FinzDataMCP
82
+ tools = openai_tools() # pass as `tools=` to any chat.completions call
83
+ result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
84
+ ```
85
+
86
+ ## Tool profiles
87
+
88
+ `lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
89
+ Tools that change data on FinzData are never exposed unless you ask for them explicitly.
90
+
91
+ ## Coverage
92
+
93
+ 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.
94
+
95
+ ## Access
96
+
97
+ 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.
98
+
99
+ With a key:
100
+
101
+ ```bash
102
+ export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
103
+ ```
104
+
105
+ or pass `finzdata_api_key="fz_..."` to `ask`.
106
+
107
+ ## Links
108
+
109
+ - Site: https://finzdata.com
110
+ - Docs: https://finzdata.com/docs
111
+ - MCP server: https://api.finzdata.com/mcp
112
+ - Python client: `pip install finzdata`
113
+ - Contact: the FinzData team, info@finzdata.com
@@ -0,0 +1,70 @@
1
+ """FinzData for Kimi: 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__", "kimi_cli_config", "write_kimi_cli_config", "CHINA_BASE_URL"]
12
+
13
+ DEFAULT_BASE_URL = "https://api.moonshot.ai/v1"
14
+ CHINA_BASE_URL = "https://api.moonshot.cn/v1"
15
+ DEFAULT_MODEL = "kimi-k3"
16
+ CLI_CONFIG_PATH = "~/.kimi/mcp.json"
17
+
18
+
19
+ def _base_url(base_url=None):
20
+ if base_url:
21
+ return base_url
22
+ return CHINA_BASE_URL if os.environ.get("FINZDATA_KIMI_REGION", "").lower() == "cn" else DEFAULT_BASE_URL
23
+
24
+
25
+ def _model(model=None):
26
+ return model or os.environ.get("FINZDATA_KIMI_MODEL") or DEFAULT_MODEL
27
+
28
+
29
+ def _llm_key(llm_api_key=None):
30
+ return llm_api_key or os.environ.get("MOONSHOT_API_KEY")
31
+
32
+
33
+ def openai_tools(profile="data", api_key=None):
34
+ """FinzData's tools in OpenAI function-calling format, fetched live from the FinzData MCP server."""
35
+ return to_openai_tools(FinzDataMCP(api_key=api_key).tools(profile))
36
+
37
+
38
+ def mcp_config(api_key=None):
39
+ """MCP server entry (``{"mcpServers": {"finzdata": ...}}``) for any MCP-capable agent, IDE or CLI."""
40
+ return mcp_server_config(api_key)
41
+
42
+
43
+ def ask(question, model=None, base_url=None, llm_api_key=None, finzdata_api_key=None, profile="data", **kw):
44
+ """Ask Kimi a market question; the model calls FinzData tools and returns its final answer."""
45
+ return _ask(question, base_url=_base_url(base_url), model=_model(model), llm_api_key=_llm_key(llm_api_key),
46
+ finzdata_api_key=finzdata_api_key, profile=profile, **kw)
47
+
48
+
49
+ def kimi_cli_config(api_key=None):
50
+ """MCP config for the Kimi CLI / Kimi Code agent."""
51
+ cfg = mcp_server_config(api_key)
52
+ cfg["mcpServers"]["finzdata"]["transport"] = "http"
53
+ return cfg
54
+
55
+
56
+ def write_kimi_cli_config(path=None, api_key=None):
57
+ """Merge FinzData into the Kimi CLI's MCP config file (other servers are kept). Returns the path."""
58
+ import json
59
+ import pathlib
60
+ p = pathlib.Path(os.path.expanduser(path or CLI_CONFIG_PATH))
61
+ data = {}
62
+ if p.exists():
63
+ try:
64
+ data = json.loads(p.read_text() or "{}")
65
+ except ValueError:
66
+ data = {}
67
+ data.setdefault("mcpServers", {}).update(kimi_cli_config(api_key)["mcpServers"])
68
+ p.parent.mkdir(parents=True, exist_ok=True)
69
+ p.write_text(json.dumps(data, indent=2))
70
+ return p
@@ -0,0 +1,18 @@
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 "--install-cli-config" in argv:
10
+ from . import write_kimi_cli_config
11
+ print(f"FinzData added to {write_kimi_cli_config()}")
12
+ return 0
13
+ return _main(argv, defaults={"base_url": _base_url(), "model": DEFAULT_MODEL, "llm_key_env": "MOONSHOT_API_KEY",
14
+ "model_env": "FINZDATA_KIMI_MODEL", "profile": "data", "prog": "finzdata-kimi"})
15
+
16
+
17
+ if __name__ == "__main__":
18
+ 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,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: finzdata-kimi
3
+ Version: 0.1.0
4
+ Summary: FinzData for Kimi: 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-kimi
10
+ Keywords: kimi,kimi-k3,moonshot,kimi-cli,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
+ Dynamic: license-file
22
+
23
+ # FinzData for Kimi
24
+
25
+ Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
26
+
27
+ FinzData gives Kimi point-in-time market data, macro vintages and server-side backtests through function calling and MCP —
28
+ so the model answers market questions with real numbers it can cite, never guesses.
29
+
30
+ ## No API key needed
31
+
32
+ FinzData works the moment you install it: no FinzData account and no FinzData key. Your model gets the free AI preview
33
+ (the last 6 months of data) automatically, and it knows to tell you when older or deeper data needs a key.
34
+ When you want more, get a free API key (email only, 12 months of data) at https://finzdata.com/account, or full
35
+ history at https://finzdata.com/shop, then set `FINZDATA_API_KEY`. Nothing else changes.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install finzdata-kimi
41
+ ```
42
+
43
+ ## Quick start
44
+
45
+ ```python
46
+ from finzdata_kimi import ask
47
+ print(ask("How did managed money positioning in crude oil change over the last 3 months?")) # no FinzData key needed
48
+ ```
49
+
50
+ ## What to ask
51
+
52
+ - "How are speculators positioned in crude oil futures by the latest COT report?"
53
+ - "Show SPY's dealer gamma, call wall and put wall for the last 30 sessions."
54
+ - "What did the first-published CPI print say last month, before revisions?"
55
+ - "Backtest a 12-month momentum strategy on US stocks, delisted names included."
56
+ - "How many ships transited the Strait of Hormuz each day last month?"
57
+ - "Who are the largest 13F holders of NVDA, and who added the most last quarter?"
58
+
59
+ ## Command line
60
+
61
+ ```bash
62
+ finzdata-kimi "How did managed money positioning in crude oil change over the last 3 months?"
63
+ finzdata-kimi --show-tools
64
+ ```
65
+
66
+ ## Moonshot API
67
+
68
+ Set `MOONSHOT_API_KEY` and ask. The default model is `kimi-k3`, which always thinks before it answers; FinzData passes its reasoning back unchanged between tool calls, as Kimi requires. Override with `model=` or `FINZDATA_KIMI_MODEL`.
69
+ Mainland China accounts: set `FINZDATA_KIMI_REGION=cn` to use `https://api.moonshot.cn/v1`.
70
+
71
+ ## Kimi CLI and Kimi Code
72
+
73
+ One command adds FinzData to the Kimi CLI's MCP config (`~/.kimi/mcp.json`), keeping every server already there:
74
+
75
+ ```bash
76
+ finzdata-kimi --install-cli-config
77
+ ```
78
+
79
+ Or add it by hand:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "finzdata": {
85
+ "url": "https://api.finzdata.com/mcp",
86
+ "type": "streamable-http",
87
+ "transport": "http"
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Any agent that accepts an MCP server URL can use `https://api.finzdata.com/mcp` the same way.
94
+
95
+ ## Kimi open weights
96
+
97
+ The Kimi models on Hugging Face (`moonshotai/Kimi-K3` and family) work through `pip install finzdata-huggingface` (Inference Providers) or any
98
+ OpenAI-compatible server you host (vLLM with tool calling enabled and the Kimi tool-call parser) using `ask(..., base_url="http://localhost:8000/v1")`.
99
+
100
+ ## Use with any OpenAI-compatible server
101
+
102
+ ```python
103
+ from finzdata_kimi import openai_tools, FinzDataMCP
104
+ tools = openai_tools() # pass as `tools=` to any chat.completions call
105
+ result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
106
+ ```
107
+
108
+ ## Tool profiles
109
+
110
+ `lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
111
+ Tools that change data on FinzData are never exposed unless you ask for them explicitly.
112
+
113
+ ## Coverage
114
+
115
+ 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.
116
+
117
+ ## Access
118
+
119
+ 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.
120
+
121
+ With a key:
122
+
123
+ ```bash
124
+ export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
125
+ ```
126
+
127
+ or pass `finzdata_api_key="fz_..."` to `ask`.
128
+
129
+ ## Links
130
+
131
+ - Site: https://finzdata.com
132
+ - Docs: https://finzdata.com/docs
133
+ - MCP server: https://api.finzdata.com/mcp
134
+ - Python client: `pip install finzdata`
135
+ - Contact: the FinzData team, info@finzdata.com
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ finzdata_kimi/__init__.py
5
+ finzdata_kimi/__main__.py
6
+ finzdata_kimi/_core.py
7
+ finzdata_kimi.egg-info/PKG-INFO
8
+ finzdata_kimi.egg-info/SOURCES.txt
9
+ finzdata_kimi.egg-info/dependency_links.txt
10
+ finzdata_kimi.egg-info/entry_points.txt
11
+ finzdata_kimi.egg-info/requires.txt
12
+ finzdata_kimi.egg-info/top_level.txt
13
+ tests/test_finzdata_kimi.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finzdata-kimi = finzdata_kimi.__main__:cli
@@ -0,0 +1 @@
1
+ finzdata_kimi
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "finzdata-kimi"
7
+ version = "0.1.0"
8
+ description = "FinzData for Kimi: point-in-time market data, macro and backtests through function calling and MCP."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "FinzData", email = "info@finzdata.com" }]
12
+ requires-python = ">=3.10"
13
+ dependencies = ["certifi"]
14
+ keywords = ["kimi", "kimi-k3", "moonshot", "kimi-cli", "finzdata", "mcp", "market-data", "function-calling", "backtesting", "ai-agents"]
15
+ classifiers = [
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Intended Audience :: Financial and Insurance Industry",
19
+ "Intended Audience :: Science/Research",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ ]
23
+
24
+ [project.optional-dependencies]
25
+
26
+ [project.scripts]
27
+ finzdata-kimi = "finzdata_kimi.__main__:cli"
28
+
29
+ [project.urls]
30
+ Homepage = "https://finzdata.com/agents"
31
+ Documentation = "https://finzdata.com/docs"
32
+ Source = "https://github.com/FinzData/finzdata-kimi"
33
+
34
+ [tool.setuptools]
35
+ packages = ["finzdata_kimi"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,65 @@
1
+ import sys, pathlib, json, types
2
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
3
+ import pytest
4
+ import finzdata_kimi as pkg
5
+ from finzdata_kimi import _core
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def clean_env(monkeypatch):
10
+ for k in ("FINZDATA_API_KEY", "MOONSHOT_API_KEY", "FINZDATA_KIMI_MODEL", "FINZDATA_KIMI_REGION", "FINZDATA_GLM_REGION",
11
+ "ZHIPUAI_API_KEY", "HUGGINGFACEHUB_API_TOKEN", "FINZDATA_LOCAL_BASE_URL"):
12
+ monkeypatch.delenv(k, raising=False)
13
+
14
+
15
+ def test_defaults_and_env(monkeypatch):
16
+ assert pkg._base_url() == pkg.DEFAULT_BASE_URL and pkg._model() == pkg.DEFAULT_MODEL
17
+ monkeypatch.setenv("FINZDATA_KIMI_MODEL", "m-env")
18
+ assert pkg._model() == "m-env" and pkg._model("m-arg") == "m-arg"
19
+ monkeypatch.setenv("MOONSHOT_API_KEY", "llm-env")
20
+ assert pkg._llm_key() == "llm-env" and pkg._llm_key("x") == "x"
21
+
22
+
23
+ def test_ask_forwards(monkeypatch):
24
+ seen = {}
25
+ monkeypatch.setattr(pkg, "_ask", lambda q, **kw: seen.update(kw, q=q) or "ok")
26
+ monkeypatch.setenv("MOONSHOT_API_KEY", "K")
27
+ assert pkg.ask("crude?", finzdata_api_key="F") == "ok"
28
+ assert seen["q"] == "crude?" and seen["base_url"] == pkg.DEFAULT_BASE_URL and seen["llm_api_key"] == "K"
29
+ assert seen["finzdata_api_key"] == "F" and seen["profile"] == "data"
30
+
31
+
32
+ def test_mcp_config(monkeypatch):
33
+ assert "headers" not in pkg.mcp_config()["mcpServers"]["finzdata"]
34
+ assert pkg.mcp_config("abc")["mcpServers"]["finzdata"]["headers"] == {"Authorization": "Bearer abc"}
35
+ assert pkg.mcp_config()["mcpServers"]["finzdata"]["url"] == pkg.MCP_URL == "https://api.finzdata.com/mcp"
36
+
37
+
38
+ def test_core_is_shared_copy():
39
+ shared = pathlib.Path(__file__).resolve().parents[2] / "openmodels_core" / "_core.py"
40
+ if shared.exists():
41
+ assert shared.read_text() == pathlib.Path(_core.__file__).read_text()
42
+
43
+
44
+ def test_cli_show_tools(monkeypatch, capsys):
45
+ from finzdata_kimi import __main__ as m
46
+ monkeypatch.setattr(_core.FinzDataMCP, "tools", lambda self, profile="data", **k: [{"name": "get_cot"}])
47
+ assert m.cli(["--show-tools"]) == 0 and "get_cot" in capsys.readouterr().out
48
+
49
+
50
+ def test_region_and_no_sampling_overrides(monkeypatch):
51
+ monkeypatch.setenv("FINZDATA_KIMI_REGION", "cn")
52
+ assert pkg._base_url() == pkg.CHINA_BASE_URL
53
+ seen = {}
54
+ monkeypatch.setattr(pkg, "_ask", lambda q, **kw: seen.update(kw) or "ok")
55
+ pkg.ask("x")
56
+ assert "extra" not in seen and pkg.DEFAULT_MODEL == "kimi-k3" # Kimi K3 rejects temperature overrides
57
+
58
+
59
+ def test_write_cli_config_merges(tmp_path):
60
+ p = tmp_path / "mcp.json"
61
+ p.write_text(json.dumps({"mcpServers": {"other": {"url": "http://x"}}}))
62
+ out = pkg.write_kimi_cli_config(str(p))
63
+ data = json.loads(out.read_text())
64
+ assert set(data["mcpServers"]) == {"other", "finzdata"}
65
+ assert data["mcpServers"]["finzdata"]["transport"] == "http"