finzdata-glm 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.
- finzdata_glm-0.1.0/LICENSE +21 -0
- finzdata_glm-0.1.0/PKG-INFO +123 -0
- finzdata_glm-0.1.0/README.md +101 -0
- finzdata_glm-0.1.0/finzdata_glm/__init__.py +45 -0
- finzdata_glm-0.1.0/finzdata_glm/__main__.py +14 -0
- finzdata_glm-0.1.0/finzdata_glm/_core.py +434 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/PKG-INFO +123 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/SOURCES.txt +13 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/dependency_links.txt +1 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/entry_points.txt +2 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/requires.txt +1 -0
- finzdata_glm-0.1.0/finzdata_glm.egg-info/top_level.txt +1 -0
- finzdata_glm-0.1.0/pyproject.toml +35 -0
- finzdata_glm-0.1.0/setup.cfg +4 -0
- finzdata_glm-0.1.0/tests/test_finzdata_glm.py +54 -0
|
@@ -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,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finzdata-glm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FinzData for GLM: 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-glm
|
|
10
|
+
Keywords: glm,zhipu,z.ai,chatglm,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 GLM
|
|
24
|
+
|
|
25
|
+
Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
|
|
26
|
+
|
|
27
|
+
FinzData gives GLM 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-glm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from finzdata_glm 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-glm "How did managed money positioning in crude oil change over the last 3 months?"
|
|
63
|
+
finzdata-glm --show-tools
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Z.ai API
|
|
67
|
+
|
|
68
|
+
Set `ZAI_API_KEY` (or `ZHIPUAI_API_KEY`) and ask. The default model is `glm-5.3`; override with `model=` or `FINZDATA_GLM_MODEL`.
|
|
69
|
+
|
|
70
|
+
## Mainland endpoint
|
|
71
|
+
|
|
72
|
+
Set `FINZDATA_GLM_REGION=cn` to use `https://open.bigmodel.cn/api/paas/v4`.
|
|
73
|
+
|
|
74
|
+
## MCP for GLM agents and IDEs
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from finzdata_glm import mcp_config
|
|
78
|
+
print(mcp_config()) # {"mcpServers": {"finzdata": {"url": "https://api.finzdata.com/mcp", ...}}}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Paste it into any MCP-capable GLM coding agent or IDE.
|
|
82
|
+
|
|
83
|
+
## GLM open weights
|
|
84
|
+
|
|
85
|
+
The GLM models on Hugging Face (`zai-org/GLM-5.3` and family) work through `pip install finzdata-huggingface`, or host them yourself with
|
|
86
|
+
vLLM (`--enable-auto-tool-choice` with the GLM tool-call parser) or SGLang and pass `base_url="http://localhost:8000/v1"`.
|
|
87
|
+
|
|
88
|
+
## Use with any OpenAI-compatible server
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from finzdata_glm import openai_tools, FinzDataMCP
|
|
92
|
+
tools = openai_tools() # pass as `tools=` to any chat.completions call
|
|
93
|
+
result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tool profiles
|
|
97
|
+
|
|
98
|
+
`lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
|
|
99
|
+
Tools that change data on FinzData are never exposed unless you ask for them explicitly.
|
|
100
|
+
|
|
101
|
+
## Coverage
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
## Access
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
With a key:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
or pass `finzdata_api_key="fz_..."` to `ask`.
|
|
116
|
+
|
|
117
|
+
## Links
|
|
118
|
+
|
|
119
|
+
- Site: https://finzdata.com
|
|
120
|
+
- Docs: https://finzdata.com/docs
|
|
121
|
+
- MCP server: https://api.finzdata.com/mcp
|
|
122
|
+
- Python client: `pip install finzdata`
|
|
123
|
+
- Contact: the FinzData team, info@finzdata.com
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# FinzData for GLM
|
|
2
|
+
|
|
3
|
+
Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
|
|
4
|
+
|
|
5
|
+
FinzData gives GLM 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-glm
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from finzdata_glm 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-glm "How did managed money positioning in crude oil change over the last 3 months?"
|
|
41
|
+
finzdata-glm --show-tools
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Z.ai API
|
|
45
|
+
|
|
46
|
+
Set `ZAI_API_KEY` (or `ZHIPUAI_API_KEY`) and ask. The default model is `glm-5.3`; override with `model=` or `FINZDATA_GLM_MODEL`.
|
|
47
|
+
|
|
48
|
+
## Mainland endpoint
|
|
49
|
+
|
|
50
|
+
Set `FINZDATA_GLM_REGION=cn` to use `https://open.bigmodel.cn/api/paas/v4`.
|
|
51
|
+
|
|
52
|
+
## MCP for GLM agents and IDEs
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from finzdata_glm import mcp_config
|
|
56
|
+
print(mcp_config()) # {"mcpServers": {"finzdata": {"url": "https://api.finzdata.com/mcp", ...}}}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Paste it into any MCP-capable GLM coding agent or IDE.
|
|
60
|
+
|
|
61
|
+
## GLM open weights
|
|
62
|
+
|
|
63
|
+
The GLM models on Hugging Face (`zai-org/GLM-5.3` and family) work through `pip install finzdata-huggingface`, or host them yourself with
|
|
64
|
+
vLLM (`--enable-auto-tool-choice` with the GLM tool-call parser) or SGLang and pass `base_url="http://localhost:8000/v1"`.
|
|
65
|
+
|
|
66
|
+
## Use with any OpenAI-compatible server
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from finzdata_glm import openai_tools, FinzDataMCP
|
|
70
|
+
tools = openai_tools() # pass as `tools=` to any chat.completions call
|
|
71
|
+
result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Tool profiles
|
|
75
|
+
|
|
76
|
+
`lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
|
|
77
|
+
Tools that change data on FinzData are never exposed unless you ask for them explicitly.
|
|
78
|
+
|
|
79
|
+
## Coverage
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
## Access
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
With a key:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
or pass `finzdata_api_key="fz_..."` to `ask`.
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
- Site: https://finzdata.com
|
|
98
|
+
- Docs: https://finzdata.com/docs
|
|
99
|
+
- MCP server: https://api.finzdata.com/mcp
|
|
100
|
+
- Python client: `pip install finzdata`
|
|
101
|
+
- Contact: the FinzData team, info@finzdata.com
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""FinzData for GLM: 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__", "CHINA_BASE_URL"]
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.z.ai/api/paas/v4"
|
|
14
|
+
CHINA_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"
|
|
15
|
+
DEFAULT_MODEL = "glm-5.3"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _base_url(base_url=None):
|
|
19
|
+
if base_url:
|
|
20
|
+
return base_url
|
|
21
|
+
return CHINA_BASE_URL if os.environ.get("FINZDATA_GLM_REGION", "").lower() == "cn" else DEFAULT_BASE_URL
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _model(model=None):
|
|
25
|
+
return model or os.environ.get("FINZDATA_GLM_MODEL") or DEFAULT_MODEL
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _llm_key(llm_api_key=None):
|
|
29
|
+
return llm_api_key or os.environ.get("ZAI_API_KEY") or os.environ.get("ZHIPUAI_API_KEY")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def openai_tools(profile="data", api_key=None):
|
|
33
|
+
"""FinzData's tools in OpenAI function-calling format, fetched live from the FinzData MCP server."""
|
|
34
|
+
return to_openai_tools(FinzDataMCP(api_key=api_key).tools(profile))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def mcp_config(api_key=None):
|
|
38
|
+
"""MCP server entry (``{"mcpServers": {"finzdata": ...}}``) for any MCP-capable agent, IDE or CLI."""
|
|
39
|
+
return mcp_server_config(api_key)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def ask(question, model=None, base_url=None, llm_api_key=None, finzdata_api_key=None, profile="data", **kw):
|
|
43
|
+
"""Ask GLM a market question; the model calls FinzData tools and returns its final answer."""
|
|
44
|
+
return _ask(question, base_url=_base_url(base_url), model=_model(model), llm_api_key=_llm_key(llm_api_key),
|
|
45
|
+
finzdata_api_key=finzdata_api_key, profile=profile, **kw)
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
return _main(argv, defaults={"base_url": _base_url(), "model": DEFAULT_MODEL, "llm_key_env": "ZAI_API_KEY",
|
|
10
|
+
"model_env": "FINZDATA_GLM_MODEL", "profile": "data", "prog": "finzdata-glm"})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
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,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: finzdata-glm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FinzData for GLM: 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-glm
|
|
10
|
+
Keywords: glm,zhipu,z.ai,chatglm,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 GLM
|
|
24
|
+
|
|
25
|
+
Built for AI, quants, data scientists and financial analysts who backtest. Clean data. Real insights. Real alpha.
|
|
26
|
+
|
|
27
|
+
FinzData gives GLM 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-glm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from finzdata_glm 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-glm "How did managed money positioning in crude oil change over the last 3 months?"
|
|
63
|
+
finzdata-glm --show-tools
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Z.ai API
|
|
67
|
+
|
|
68
|
+
Set `ZAI_API_KEY` (or `ZHIPUAI_API_KEY`) and ask. The default model is `glm-5.3`; override with `model=` or `FINZDATA_GLM_MODEL`.
|
|
69
|
+
|
|
70
|
+
## Mainland endpoint
|
|
71
|
+
|
|
72
|
+
Set `FINZDATA_GLM_REGION=cn` to use `https://open.bigmodel.cn/api/paas/v4`.
|
|
73
|
+
|
|
74
|
+
## MCP for GLM agents and IDEs
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from finzdata_glm import mcp_config
|
|
78
|
+
print(mcp_config()) # {"mcpServers": {"finzdata": {"url": "https://api.finzdata.com/mcp", ...}}}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Paste it into any MCP-capable GLM coding agent or IDE.
|
|
82
|
+
|
|
83
|
+
## GLM open weights
|
|
84
|
+
|
|
85
|
+
The GLM models on Hugging Face (`zai-org/GLM-5.3` and family) work through `pip install finzdata-huggingface`, or host them yourself with
|
|
86
|
+
vLLM (`--enable-auto-tool-choice` with the GLM tool-call parser) or SGLang and pass `base_url="http://localhost:8000/v1"`.
|
|
87
|
+
|
|
88
|
+
## Use with any OpenAI-compatible server
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from finzdata_glm import openai_tools, FinzDataMCP
|
|
92
|
+
tools = openai_tools() # pass as `tools=` to any chat.completions call
|
|
93
|
+
result = FinzDataMCP().call("get_cot", {"market": "crude"}) # run the tool the model picked
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Tool profiles
|
|
97
|
+
|
|
98
|
+
`lite` (6 core tools, for small models), `data` (20 data and backtest tools, the default) and `full` (every read tool).
|
|
99
|
+
Tools that change data on FinzData are never exposed unless you ask for them explicitly.
|
|
100
|
+
|
|
101
|
+
## Coverage
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
## Access
|
|
106
|
+
|
|
107
|
+
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.
|
|
108
|
+
|
|
109
|
+
With a key:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
export FINZDATA_API_KEY=fz_... # free at https://finzdata.com/account
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
or pass `finzdata_api_key="fz_..."` to `ask`.
|
|
116
|
+
|
|
117
|
+
## Links
|
|
118
|
+
|
|
119
|
+
- Site: https://finzdata.com
|
|
120
|
+
- Docs: https://finzdata.com/docs
|
|
121
|
+
- MCP server: https://api.finzdata.com/mcp
|
|
122
|
+
- Python client: `pip install finzdata`
|
|
123
|
+
- Contact: the FinzData team, info@finzdata.com
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
finzdata_glm/__init__.py
|
|
5
|
+
finzdata_glm/__main__.py
|
|
6
|
+
finzdata_glm/_core.py
|
|
7
|
+
finzdata_glm.egg-info/PKG-INFO
|
|
8
|
+
finzdata_glm.egg-info/SOURCES.txt
|
|
9
|
+
finzdata_glm.egg-info/dependency_links.txt
|
|
10
|
+
finzdata_glm.egg-info/entry_points.txt
|
|
11
|
+
finzdata_glm.egg-info/requires.txt
|
|
12
|
+
finzdata_glm.egg-info/top_level.txt
|
|
13
|
+
tests/test_finzdata_glm.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
certifi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
finzdata_glm
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "finzdata-glm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "FinzData for GLM: 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 = ["glm", "zhipu", "z.ai", "chatglm", "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-glm = "finzdata_glm.__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-glm"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["finzdata_glm"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys, pathlib, json, types
|
|
2
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
|
|
3
|
+
import pytest
|
|
4
|
+
import finzdata_glm as pkg
|
|
5
|
+
from finzdata_glm import _core
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def clean_env(monkeypatch):
|
|
10
|
+
for k in ("FINZDATA_API_KEY", "ZAI_API_KEY", "FINZDATA_GLM_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_GLM_MODEL", "m-env")
|
|
18
|
+
assert pkg._model() == "m-env" and pkg._model("m-arg") == "m-arg"
|
|
19
|
+
monkeypatch.setenv("ZAI_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("ZAI_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_glm 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_fallback_key(monkeypatch):
|
|
51
|
+
monkeypatch.setenv("FINZDATA_GLM_REGION", "cn")
|
|
52
|
+
assert pkg._base_url() == pkg.CHINA_BASE_URL
|
|
53
|
+
monkeypatch.setenv("ZHIPUAI_API_KEY", "z")
|
|
54
|
+
assert pkg._llm_key() == "z"
|