equity-intel-mcp 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.
- equity_intel_mcp-0.1.0/LICENSE +21 -0
- equity_intel_mcp-0.1.0/PKG-INFO +170 -0
- equity_intel_mcp-0.1.0/README.md +137 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/__init__.py +9 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/__main__.py +5 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/analyze.py +90 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/cache.py +41 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/formatting.py +124 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/server.py +318 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/signal.py +74 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/__init__.py +1 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/analysts.py +62 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/insider.py +119 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/options.py +109 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/quote.py +81 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/superinvestor.py +112 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp/sources/valuation.py +118 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/PKG-INFO +170 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/SOURCES.txt +25 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/dependency_links.txt +1 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/entry_points.txt +2 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/requires.txt +8 -0
- equity_intel_mcp-0.1.0/equity_intel_mcp.egg-info/top_level.txt +1 -0
- equity_intel_mcp-0.1.0/pyproject.toml +47 -0
- equity_intel_mcp-0.1.0/setup.cfg +4 -0
- equity_intel_mcp-0.1.0/tests/test_composite.py +42 -0
- equity_intel_mcp-0.1.0/tests/test_smoke.py +44 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cristian Amigo
|
|
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,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: equity-intel-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Institutional-grade equity analysis (insider, superinvestor, valuation) over MCP.
|
|
5
|
+
Author-email: Cristian Amigo <cstamigo@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cstamigo-droid/equity-intel-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/cstamigo-droid/equity-intel-mcp
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/cstamigo-droid/equity-intel-mcp/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,equity,stocks,sec,edgar,insider-trading,valuation,llm,agents
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: mcp>=1.2.0
|
|
25
|
+
Requires-Dist: pydantic>=2.6
|
|
26
|
+
Requires-Dist: python-dotenv>=1.0
|
|
27
|
+
Requires-Dist: yfinance>=0.2.40
|
|
28
|
+
Requires-Dist: requests>=2.31
|
|
29
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
30
|
+
Requires-Dist: edgartools>=4.0
|
|
31
|
+
Requires-Dist: pandas>=2.0
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# equity-intel-mcp
|
|
35
|
+
|
|
36
|
+
[](https://glama.ai/mcp/servers/cstamigo-droid/equity-intel-mcp)
|
|
37
|
+
|
|
38
|
+
[](LICENSE) [](https://www.python.org) [](https://modelcontextprotocol.io)
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
**Institutional-grade equity analysis for any LLM, over the Model Context Protocol.**
|
|
44
|
+
|
|
45
|
+
Most "stock" integrations just echo a price. This one gives an AI agent the
|
|
46
|
+
signals professionals actually look at — **insider buying from SEC filings**,
|
|
47
|
+
**what renowned value investors are holding**, analyst consensus, options-implied
|
|
48
|
+
moves, and valuation — and blends them into a single, confidence-weighted verdict.
|
|
49
|
+
|
|
50
|
+
It runs entirely on **free / public data** (Yahoo Finance, SEC EDGAR, Dataroma),
|
|
51
|
+
fails gracefully when a source is missing, and never fabricates a signal it
|
|
52
|
+
doesn't have.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
# Equity Intelligence — MSFT
|
|
56
|
+
|
|
57
|
+
## Verdict: Neutral → HOLD
|
|
58
|
+
[........|#.......] +11/100 · confidence 69% · 5 source(s)
|
|
59
|
+
|
|
60
|
+
| Source | Signal | Score | Conf | Weight |
|
|
61
|
+
|---------------|--------------|-------:|-----:|-------:|
|
|
62
|
+
| insider | Bearish | -77 | 70% | 0.25 |
|
|
63
|
+
| superinvestor | Lean bullish | +33 | 100% | 0.30 |
|
|
64
|
+
| analysts | Bullish | +64 | 75% | 0.15 |
|
|
65
|
+
| valuation | Bullish | +91 | 60% | 0.09 |
|
|
66
|
+
| options | Lean bullish | +19 | 40% | 0.04 |
|
|
67
|
+
|
|
68
|
+
- insider: Insiders net selling $13.5M over 180d (62 filings).
|
|
69
|
+
- superinvestor: 38 tracked superinvestors hold MSFT; 19 buys / 18 sells.
|
|
70
|
+
- analysts: 66 analysts: 23 strong buy / 38 buy / 5 hold.
|
|
71
|
+
- valuation: Fair value ~$569 vs $391 (+46% upside); health 84/100.
|
|
72
|
+
- options: 1-month implied move +/-7.6%; put/call OI 0.53.
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Tools
|
|
78
|
+
|
|
79
|
+
| Tool | What it does | Source | Status |
|
|
80
|
+
|------|--------------|--------|:------:|
|
|
81
|
+
| `equity_analyze_ticker` | **Hero tool.** Runs every source in parallel and returns one scored verdict (BUY → AVOID) with a per-source breakdown. | composite | ✅ |
|
|
82
|
+
| `equity_insider_activity` | Net insider buying vs. selling from SEC **Form 4** filings (180-day window), weighted by USD value. | SEC EDGAR | ✅ |
|
|
83
|
+
| `equity_superinvestors` | Which of ~80 tracked value investors hold the stock, plus recent net buying/selling. | Dataroma | ✅ |
|
|
84
|
+
| `equity_get_quote` | Live price snapshot + position in the 52-week range. | Yahoo Finance | ✅ |
|
|
85
|
+
| `equity_analyst_consensus` | Wall Street buy/hold/sell consensus — scored from distribution of strong-buy to strong-sell ratings. | Finnhub | ✅ |
|
|
86
|
+
| `equity_options_signal` | 1-month implied move (straddle/spot) + put/call OI skew. Primary use: risk-sizing. | Yahoo Finance | ✅ |
|
|
87
|
+
| `equity_valuation` | Fair-value estimate (forward EPS × sector P/E) + financial-health score (debt, liquidity, margins). | Yahoo Finance | ✅ |
|
|
88
|
+
|
|
89
|
+
Every tool returns **Markdown** (human-readable, default) or **JSON**
|
|
90
|
+
(`response_format="json"`) for programmatic use.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Quick start
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/cstamigo-droid/equity-intel-mcp equity-intel-mcp
|
|
98
|
+
cd equity-intel-mcp
|
|
99
|
+
python -m venv .venv && .venv\Scripts\activate # Windows
|
|
100
|
+
pip install -r requirements.txt
|
|
101
|
+
|
|
102
|
+
copy .env.example .env # then edit .env (set EDGAR_IDENTITY)
|
|
103
|
+
python -m equity_intel_mcp # starts the MCP server over stdio
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Smoke test** (hits the live sources and prints each signal):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python tests/test_smoke.py AAPL
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Configure (`.env`)
|
|
113
|
+
|
|
114
|
+
```ini
|
|
115
|
+
# Required by SEC fair-access policy — any "Name email@example.com"
|
|
116
|
+
EDGAR_IDENTITY=Your Name you@example.com
|
|
117
|
+
# Required by equity_analyst_consensus (free key at finnhub.io)
|
|
118
|
+
FINNHUB_API_KEY=your-key-here
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Use it in Claude Desktop
|
|
124
|
+
|
|
125
|
+
Add this to `claude_desktop_config.json`
|
|
126
|
+
(`%APPDATA%\Claude\` on Windows, `~/Library/Application Support/Claude/` on macOS),
|
|
127
|
+
then restart Claude Desktop:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcpServers": {
|
|
132
|
+
"equity-intel": {
|
|
133
|
+
"command": "python",
|
|
134
|
+
"args": ["-m", "equity_intel_mcp"],
|
|
135
|
+
"cwd": "C:/path/to/equity-intel-mcp",
|
|
136
|
+
"env": { "EDGAR_IDENTITY": "Your Name you@example.com" }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Then just ask Claude: *"Give me a full read on NVDA"* or *"Are insiders buying PLTR?"*
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Why it's built this way
|
|
147
|
+
|
|
148
|
+
- **Uniform signal contract.** Every source returns the same shape
|
|
149
|
+
(`score -100..+100`, `confidence 0..1`, `data`, `summary`). That's what lets an
|
|
150
|
+
LLM reason *across* heterogeneous evidence instead of parsing five formats.
|
|
151
|
+
- **Graceful degradation.** A stock with no Form 4 activity returns *"no signal"*,
|
|
152
|
+
not a fake bearish score. Missing data lowers confidence; it never invents a call.
|
|
153
|
+
- **Confidence-weighted blending.** The composite weights each source by its
|
|
154
|
+
importance × its own confidence, so thin signals don't outvote strong ones.
|
|
155
|
+
- **Resilient + cached.** Short per-source TTL caches avoid hammering rate-limited
|
|
156
|
+
endpoints when an agent calls several tools on the same ticker in one turn.
|
|
157
|
+
|
|
158
|
+
See [ROADMAP.md](ROADMAP.md) for what's next.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Disclaimer
|
|
163
|
+
|
|
164
|
+
For research and educational use only. **Not investment advice.** Data comes from
|
|
165
|
+
third-party public sources and may be delayed or incomplete. Do your own due
|
|
166
|
+
diligence.
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# equity-intel-mcp
|
|
2
|
+
|
|
3
|
+
[](https://glama.ai/mcp/servers/cstamigo-droid/equity-intel-mcp)
|
|
4
|
+
|
|
5
|
+
[](LICENSE) [](https://www.python.org) [](https://modelcontextprotocol.io)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
**Institutional-grade equity analysis for any LLM, over the Model Context Protocol.**
|
|
11
|
+
|
|
12
|
+
Most "stock" integrations just echo a price. This one gives an AI agent the
|
|
13
|
+
signals professionals actually look at — **insider buying from SEC filings**,
|
|
14
|
+
**what renowned value investors are holding**, analyst consensus, options-implied
|
|
15
|
+
moves, and valuation — and blends them into a single, confidence-weighted verdict.
|
|
16
|
+
|
|
17
|
+
It runs entirely on **free / public data** (Yahoo Finance, SEC EDGAR, Dataroma),
|
|
18
|
+
fails gracefully when a source is missing, and never fabricates a signal it
|
|
19
|
+
doesn't have.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
# Equity Intelligence — MSFT
|
|
23
|
+
|
|
24
|
+
## Verdict: Neutral → HOLD
|
|
25
|
+
[........|#.......] +11/100 · confidence 69% · 5 source(s)
|
|
26
|
+
|
|
27
|
+
| Source | Signal | Score | Conf | Weight |
|
|
28
|
+
|---------------|--------------|-------:|-----:|-------:|
|
|
29
|
+
| insider | Bearish | -77 | 70% | 0.25 |
|
|
30
|
+
| superinvestor | Lean bullish | +33 | 100% | 0.30 |
|
|
31
|
+
| analysts | Bullish | +64 | 75% | 0.15 |
|
|
32
|
+
| valuation | Bullish | +91 | 60% | 0.09 |
|
|
33
|
+
| options | Lean bullish | +19 | 40% | 0.04 |
|
|
34
|
+
|
|
35
|
+
- insider: Insiders net selling $13.5M over 180d (62 filings).
|
|
36
|
+
- superinvestor: 38 tracked superinvestors hold MSFT; 19 buys / 18 sells.
|
|
37
|
+
- analysts: 66 analysts: 23 strong buy / 38 buy / 5 hold.
|
|
38
|
+
- valuation: Fair value ~$569 vs $391 (+46% upside); health 84/100.
|
|
39
|
+
- options: 1-month implied move +/-7.6%; put/call OI 0.53.
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
| Tool | What it does | Source | Status |
|
|
47
|
+
|------|--------------|--------|:------:|
|
|
48
|
+
| `equity_analyze_ticker` | **Hero tool.** Runs every source in parallel and returns one scored verdict (BUY → AVOID) with a per-source breakdown. | composite | ✅ |
|
|
49
|
+
| `equity_insider_activity` | Net insider buying vs. selling from SEC **Form 4** filings (180-day window), weighted by USD value. | SEC EDGAR | ✅ |
|
|
50
|
+
| `equity_superinvestors` | Which of ~80 tracked value investors hold the stock, plus recent net buying/selling. | Dataroma | ✅ |
|
|
51
|
+
| `equity_get_quote` | Live price snapshot + position in the 52-week range. | Yahoo Finance | ✅ |
|
|
52
|
+
| `equity_analyst_consensus` | Wall Street buy/hold/sell consensus — scored from distribution of strong-buy to strong-sell ratings. | Finnhub | ✅ |
|
|
53
|
+
| `equity_options_signal` | 1-month implied move (straddle/spot) + put/call OI skew. Primary use: risk-sizing. | Yahoo Finance | ✅ |
|
|
54
|
+
| `equity_valuation` | Fair-value estimate (forward EPS × sector P/E) + financial-health score (debt, liquidity, margins). | Yahoo Finance | ✅ |
|
|
55
|
+
|
|
56
|
+
Every tool returns **Markdown** (human-readable, default) or **JSON**
|
|
57
|
+
(`response_format="json"`) for programmatic use.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Quick start
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/cstamigo-droid/equity-intel-mcp equity-intel-mcp
|
|
65
|
+
cd equity-intel-mcp
|
|
66
|
+
python -m venv .venv && .venv\Scripts\activate # Windows
|
|
67
|
+
pip install -r requirements.txt
|
|
68
|
+
|
|
69
|
+
copy .env.example .env # then edit .env (set EDGAR_IDENTITY)
|
|
70
|
+
python -m equity_intel_mcp # starts the MCP server over stdio
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Smoke test** (hits the live sources and prints each signal):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python tests/test_smoke.py AAPL
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Configure (`.env`)
|
|
80
|
+
|
|
81
|
+
```ini
|
|
82
|
+
# Required by SEC fair-access policy — any "Name email@example.com"
|
|
83
|
+
EDGAR_IDENTITY=Your Name you@example.com
|
|
84
|
+
# Required by equity_analyst_consensus (free key at finnhub.io)
|
|
85
|
+
FINNHUB_API_KEY=your-key-here
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Use it in Claude Desktop
|
|
91
|
+
|
|
92
|
+
Add this to `claude_desktop_config.json`
|
|
93
|
+
(`%APPDATA%\Claude\` on Windows, `~/Library/Application Support/Claude/` on macOS),
|
|
94
|
+
then restart Claude Desktop:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"equity-intel": {
|
|
100
|
+
"command": "python",
|
|
101
|
+
"args": ["-m", "equity_intel_mcp"],
|
|
102
|
+
"cwd": "C:/path/to/equity-intel-mcp",
|
|
103
|
+
"env": { "EDGAR_IDENTITY": "Your Name you@example.com" }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then just ask Claude: *"Give me a full read on NVDA"* or *"Are insiders buying PLTR?"*
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Why it's built this way
|
|
114
|
+
|
|
115
|
+
- **Uniform signal contract.** Every source returns the same shape
|
|
116
|
+
(`score -100..+100`, `confidence 0..1`, `data`, `summary`). That's what lets an
|
|
117
|
+
LLM reason *across* heterogeneous evidence instead of parsing five formats.
|
|
118
|
+
- **Graceful degradation.** A stock with no Form 4 activity returns *"no signal"*,
|
|
119
|
+
not a fake bearish score. Missing data lowers confidence; it never invents a call.
|
|
120
|
+
- **Confidence-weighted blending.** The composite weights each source by its
|
|
121
|
+
importance × its own confidence, so thin signals don't outvote strong ones.
|
|
122
|
+
- **Resilient + cached.** Short per-source TTL caches avoid hammering rate-limited
|
|
123
|
+
endpoints when an agent calls several tools on the same ticker in one turn.
|
|
124
|
+
|
|
125
|
+
See [ROADMAP.md](ROADMAP.md) for what's next.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Disclaimer
|
|
130
|
+
|
|
131
|
+
For research and educational use only. **Not investment advice.** Data comes from
|
|
132
|
+
third-party public sources and may be delayed or incomplete. Do your own due
|
|
133
|
+
diligence.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""equity_intel_mcp — institutional-grade equity analysis over the Model Context Protocol.
|
|
2
|
+
|
|
3
|
+
Gives any MCP client (Claude Desktop, Claude Code, agents) clean tools for:
|
|
4
|
+
insider activity (SEC Form 4), superinvestor holdings (Dataroma), Wall Street
|
|
5
|
+
consensus, options-implied moves, valuation & financial health, and a composite
|
|
6
|
+
scored verdict — all from free/public data sources with graceful degradation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Composite verdict — blends every directional source into one scored call.
|
|
2
|
+
|
|
3
|
+
The blend is a confidence-weighted average: each source's directional score is
|
|
4
|
+
weighted by (source importance x its own confidence), so a high-conviction
|
|
5
|
+
insider signal moves the needle more than a thin, low-confidence one. Sources
|
|
6
|
+
that returned no data are simply absent from the average — missing data lowers
|
|
7
|
+
overall confidence but never drags the score toward a false bearish/bullish read.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from statistics import mean
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .signal import Signal, clamp
|
|
15
|
+
|
|
16
|
+
# Relative importance of each directional source in the final verdict.
|
|
17
|
+
# Sources not yet implemented are listed so `coverage` reflects the full design.
|
|
18
|
+
SOURCE_WEIGHTS: dict[str, float] = {
|
|
19
|
+
"insider": 0.35,
|
|
20
|
+
"superinvestor": 0.30,
|
|
21
|
+
"analysts": 0.20,
|
|
22
|
+
"valuation": 0.15,
|
|
23
|
+
"options": 0.10,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _verdict(score: float) -> tuple[str, str]:
|
|
28
|
+
"""Map a composite score to (verdict label, recommended action)."""
|
|
29
|
+
if score >= 40:
|
|
30
|
+
return "Bullish", "BUY"
|
|
31
|
+
if score >= 15:
|
|
32
|
+
return "Lean bullish", "ACCUMULATE"
|
|
33
|
+
if score > -15:
|
|
34
|
+
return "Neutral", "HOLD"
|
|
35
|
+
if score > -40:
|
|
36
|
+
return "Lean bearish", "TRIM"
|
|
37
|
+
return "Bearish", "AVOID"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def composite(signals: list[Signal]) -> dict[str, Any]:
|
|
41
|
+
"""Blend directional signals into a single verdict.
|
|
42
|
+
|
|
43
|
+
Returns a dict: score (-100..100), confidence (0..1), verdict, action,
|
|
44
|
+
n_sources, coverage, and a per-source breakdown list.
|
|
45
|
+
"""
|
|
46
|
+
directional = [s for s in signals if s.directional and s.ok]
|
|
47
|
+
if not directional:
|
|
48
|
+
return {
|
|
49
|
+
"score": 0.0,
|
|
50
|
+
"confidence": 0.0,
|
|
51
|
+
"verdict": "No data",
|
|
52
|
+
"action": "INSUFFICIENT DATA",
|
|
53
|
+
"n_sources": 0,
|
|
54
|
+
"coverage": 0.0,
|
|
55
|
+
"breakdown": [],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
num = den = 0.0
|
|
59
|
+
breakdown = []
|
|
60
|
+
for s in directional:
|
|
61
|
+
weight = SOURCE_WEIGHTS.get(s.source, 0.1) * s.confidence
|
|
62
|
+
num += s.score * weight
|
|
63
|
+
den += weight
|
|
64
|
+
breakdown.append(
|
|
65
|
+
{
|
|
66
|
+
"source": s.source,
|
|
67
|
+
"score": s.score,
|
|
68
|
+
"confidence": s.confidence,
|
|
69
|
+
"effective_weight": round(weight, 3),
|
|
70
|
+
"summary": s.summary,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
score = clamp(num / den if den else 0.0, -100.0, 100.0)
|
|
75
|
+
verdict, action = _verdict(score)
|
|
76
|
+
|
|
77
|
+
# Confidence reflects both per-source quality and how many of the designed
|
|
78
|
+
# sources actually reported (coverage).
|
|
79
|
+
coverage = len(directional) / len(SOURCE_WEIGHTS)
|
|
80
|
+
confidence = mean(s.confidence for s in directional) * (0.5 + 0.5 * coverage)
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"score": round(score, 1),
|
|
84
|
+
"confidence": round(confidence, 2),
|
|
85
|
+
"verdict": verdict,
|
|
86
|
+
"action": action,
|
|
87
|
+
"n_sources": len(directional),
|
|
88
|
+
"coverage": round(coverage, 2),
|
|
89
|
+
"breakdown": breakdown,
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Tiny thread-safe TTL cache.
|
|
2
|
+
|
|
3
|
+
External data sources (Yahoo, SEC, Dataroma) are slow and rate-limited. A short
|
|
4
|
+
in-process cache means an agent that calls several tools on the same ticker in
|
|
5
|
+
one turn — or the composite `analyze_ticker` that fans out to every source —
|
|
6
|
+
doesn't refetch the same data seconds apart. TTLs are per-source (a quote ages
|
|
7
|
+
in 60s; a 13F holding list can live for an hour).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
_lock = threading.Lock()
|
|
16
|
+
_store: dict[str, tuple[float, Any]] = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_or_fetch(key: str, ttl_s: float, fetch: Callable[[], Any]) -> Any:
|
|
20
|
+
"""Return a cached value if fresh, otherwise call `fetch()` and store it.
|
|
21
|
+
|
|
22
|
+
`fetch` runs OUTSIDE the lock so a slow network call never blocks other
|
|
23
|
+
cache readers. A rare duplicate fetch under concurrency is acceptable.
|
|
24
|
+
"""
|
|
25
|
+
now = time.monotonic()
|
|
26
|
+
with _lock:
|
|
27
|
+
hit = _store.get(key)
|
|
28
|
+
if hit is not None and (now - hit[0]) < ttl_s:
|
|
29
|
+
return hit[1]
|
|
30
|
+
|
|
31
|
+
value = fetch()
|
|
32
|
+
|
|
33
|
+
with _lock:
|
|
34
|
+
_store[key] = (time.monotonic(), value)
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def clear() -> None:
|
|
39
|
+
"""Drop all cached entries (used by tests)."""
|
|
40
|
+
with _lock:
|
|
41
|
+
_store.clear()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Render Signals as Markdown (human) or JSON (machine) for MCP tool output."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from .signal import Signal, clamp, label
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponseFormat(str, Enum):
|
|
11
|
+
"""Output format for tool responses."""
|
|
12
|
+
|
|
13
|
+
MARKDOWN = "markdown"
|
|
14
|
+
JSON = "json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _gauge(score: float) -> str:
|
|
18
|
+
"""A compact ASCII gauge for a -100..+100 score.
|
|
19
|
+
|
|
20
|
+
Bearish fills left of center, bullish fills right, e.g.
|
|
21
|
+
-100 -> `[########|........]`, +50 -> `[........|####....]`.
|
|
22
|
+
"""
|
|
23
|
+
n = 8 # half-width
|
|
24
|
+
s = clamp(score, -100, 100)
|
|
25
|
+
filled = int(round(abs(s) / 100 * n))
|
|
26
|
+
if s < 0:
|
|
27
|
+
left, right = "." * (n - filled) + "#" * filled, "." * n
|
|
28
|
+
elif s > 0:
|
|
29
|
+
left, right = "." * n, "#" * filled + "." * (n - filled)
|
|
30
|
+
else:
|
|
31
|
+
left = right = "." * n
|
|
32
|
+
return f"[{left}|{right}]"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render(signal: Signal, title: str, fmt: ResponseFormat) -> str:
|
|
36
|
+
"""Format a single Signal for return to an MCP client."""
|
|
37
|
+
if fmt == ResponseFormat.JSON:
|
|
38
|
+
return json.dumps(signal.to_dict(), indent=2, default=str)
|
|
39
|
+
|
|
40
|
+
lines = [f"# {title}"]
|
|
41
|
+
if not signal.ok:
|
|
42
|
+
lines += ["", f"⚠️ No signal available — {signal.error}"]
|
|
43
|
+
return "\n".join(lines)
|
|
44
|
+
|
|
45
|
+
if signal.directional:
|
|
46
|
+
lines += [
|
|
47
|
+
"",
|
|
48
|
+
f"**Signal:** {label(signal.score)} "
|
|
49
|
+
f"`{_gauge(signal.score)}` {signal.score:+.0f}/100 "
|
|
50
|
+
f"· confidence {signal.confidence:.0%}",
|
|
51
|
+
]
|
|
52
|
+
if signal.summary:
|
|
53
|
+
lines += ["", signal.summary]
|
|
54
|
+
|
|
55
|
+
if signal.data:
|
|
56
|
+
lines += ["", "## Details"]
|
|
57
|
+
for k, v in signal.data.items():
|
|
58
|
+
if v is None or k == "url":
|
|
59
|
+
continue
|
|
60
|
+
lines.append(f"- **{k.replace('_', ' ')}:** {_fmt_value(v)}")
|
|
61
|
+
url = signal.data.get("url")
|
|
62
|
+
if url:
|
|
63
|
+
lines += ["", f"Source: {url}"]
|
|
64
|
+
|
|
65
|
+
return "\n".join(lines)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def render_composite(ticker: str, result: dict, signals: list[Signal], fmt: ResponseFormat) -> str:
|
|
69
|
+
"""Render the composite `analyze_ticker` verdict plus its source breakdown."""
|
|
70
|
+
if fmt == ResponseFormat.JSON:
|
|
71
|
+
return json.dumps(
|
|
72
|
+
{"ticker": ticker, "verdict": result, "signals": [s.to_dict() for s in signals]},
|
|
73
|
+
indent=2,
|
|
74
|
+
default=str,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
lines = [
|
|
78
|
+
f"# Equity Intelligence — {ticker}",
|
|
79
|
+
"",
|
|
80
|
+
f"## Verdict: {result['verdict']} → **{result['action']}**",
|
|
81
|
+
f"`{_gauge(result['score'])}` {result['score']:+.0f}/100 "
|
|
82
|
+
f"· confidence {result['confidence']:.0%} "
|
|
83
|
+
f"· {result['n_sources']} source(s), {result['coverage']:.0%} coverage",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
quote_sig = next((s for s in signals if s.source == "quote" and s.ok), None)
|
|
87
|
+
if quote_sig:
|
|
88
|
+
lines += ["", f"**Price context:** {quote_sig.summary}"]
|
|
89
|
+
|
|
90
|
+
if result["breakdown"]:
|
|
91
|
+
lines += [
|
|
92
|
+
"",
|
|
93
|
+
"## Signal breakdown",
|
|
94
|
+
"| Source | Signal | Score | Conf | Weight |",
|
|
95
|
+
"|---|---|---:|---:|---:|",
|
|
96
|
+
]
|
|
97
|
+
for b in result["breakdown"]:
|
|
98
|
+
lines.append(
|
|
99
|
+
f"| {b['source']} | {label(b['score'])} | {b['score']:+.0f} "
|
|
100
|
+
f"| {b['confidence']:.0%} | {b['effective_weight']:.2f} |"
|
|
101
|
+
)
|
|
102
|
+
lines += ["", "## What each source says"]
|
|
103
|
+
for b in result["breakdown"]:
|
|
104
|
+
lines.append(f"- **{b['source']}:** {b['summary']}")
|
|
105
|
+
|
|
106
|
+
skipped = [s for s in signals if s.directional and not s.ok]
|
|
107
|
+
if skipped:
|
|
108
|
+
lines += ["", "## No signal (data unavailable)"]
|
|
109
|
+
for s in skipped:
|
|
110
|
+
lines.append(f"- **{s.source}:** {s.error}")
|
|
111
|
+
|
|
112
|
+
return "\n".join(lines)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fmt_value(v: object) -> str:
|
|
116
|
+
if isinstance(v, bool):
|
|
117
|
+
return "yes" if v else "no"
|
|
118
|
+
if isinstance(v, float):
|
|
119
|
+
return f"{v:,.2f}"
|
|
120
|
+
if isinstance(v, int):
|
|
121
|
+
return f"{v:,}"
|
|
122
|
+
if isinstance(v, list):
|
|
123
|
+
return ", ".join(str(x) for x in v[:12]) + (" …" if len(v) > 12 else "")
|
|
124
|
+
return str(v)
|