Add finance enhancement MCP tools
This commit is contained in:
@@ -229,6 +229,11 @@ After connecting, just tell your AI assistant:
|
||||
| | `search_news_advanced` | Multi-filter: coins + keywords + engine types combined |
|
||||
| AI | `get_high_score_news` | High AI impact score articles (0-100 scale) |
|
||||
| | `get_news_by_signal` | By AI trading signal: long / short / neutral |
|
||||
| Finance | `search_companies` | Search public company candidates by keyword, ticker, CIK, or fuzzy name |
|
||||
| | `get_company_info` | Resolve a company and list available SEC filings, research reports, transcripts, and financial fields |
|
||||
| | `get_company_report_text` | Fetch SEC filing, third-party research report, or earnings-call transcript text |
|
||||
| | `get_crypto_holdings` | Query wallet-visible on-chain holdings evidence for an institution or address |
|
||||
| | `get_crypto_holding_changes` | Query adjacent-snapshot on-chain holdings changes |
|
||||
| Real-time | `subscribe_latest_news` | WebSocket live feed with coin & engine type filters |
|
||||
|
||||
> For a comprehensive usage guide with detailed examples, see [Knowledge Guide](./knowledge/guide.md).
|
||||
|
||||
@@ -229,6 +229,11 @@ opennews-mcp という MCP サーバーをレビューしてインストール
|
||||
| | `search_news_advanced` | 複合フィルタ:通貨 + キーワード + エンジンタイプ |
|
||||
| AI | `get_high_score_news` | 高 AI 影響度スコア記事(0-100 スケール) |
|
||||
| | `get_news_by_signal` | AI トレーディングシグナル別:long / short / neutral |
|
||||
| Finance | `search_companies` | キーワード、ticker、CIK、曖昧な会社名で上場企業候補を検索 |
|
||||
| | `get_company_info` | 会社を解決し、利用可能な SEC filing、リサーチレポート、決算説明会 transcript、財務項目を一覧 |
|
||||
| | `get_company_report_text` | SEC filing、第三者リサーチ、決算説明会 transcript の本文を取得 |
|
||||
| | `get_crypto_holdings` | 機関またはアドレスのウォレット可視オンチェーン保有証拠を照会 |
|
||||
| | `get_crypto_holding_changes` | 隣接スナップショット間のオンチェーン保有変化を照会 |
|
||||
| リアルタイム | `subscribe_latest_news` | WebSocket ライブフィード、通貨・エンジンタイプフィルタ対応 |
|
||||
|
||||
> 詳細な使用ガイドとサンプルについては、[ナレッジガイド](../knowledge/guide.md) をご覧ください。
|
||||
|
||||
@@ -229,6 +229,11 @@ opennews-mcp MCP 서버를 검토하고 설치해주세요. 프로젝트는 로
|
||||
| | `search_news_advanced` | 복합 필터: 코인 + 키워드 + 엔진 유형 조합 |
|
||||
| AI | `get_high_score_news` | 높은 AI 영향도 점수 기사 (0-100 스케일) |
|
||||
| | `get_news_by_signal` | AI 트레이딩 시그널별: long / short / neutral |
|
||||
| 금융 강화 | `search_companies` | 키워드, ticker, CIK 또는 유사 회사명으로 상장사 후보 검색 |
|
||||
| | `get_company_info` | 회사를 식별하고 사용 가능한 SEC filing, 리서치 보고서, 실적 콜 transcript, 재무 항목을 나열 |
|
||||
| | `get_company_report_text` | SEC filing, 서드파티 리서치, 실적 콜 transcript 본문 조회 |
|
||||
| | `get_crypto_holdings` | 기관 또는 주소의 지갑 기준 온체인 보유 증거 조회 |
|
||||
| | `get_crypto_holding_changes` | 인접 스냅샷 사이의 온체인 보유 변화 조회 |
|
||||
| 실시간 | `subscribe_latest_news` | WebSocket 라이브 피드, 코인 및 엔진 유형 필터 지원 |
|
||||
|
||||
> 자세한 사용 가이드와 예제는 [지식 가이드](../knowledge/guide.md)를 참조하세요.
|
||||
|
||||
@@ -229,6 +229,11 @@ cp -r openclaw-skill/opennews ~/.openclaw/skills/
|
||||
| | `search_news_advanced` | 多条件组合: 币种 + 关键词 + 引擎类型 |
|
||||
| AI | `get_high_score_news` | 高 AI 影响力评分文章 (0-100 分制) |
|
||||
| | `get_news_by_signal` | 按 AI 交易信号: long / short / neutral |
|
||||
| 金融增强 | `search_companies` | 按关键词、ticker、CIK 或模糊公司名搜索上市公司候选项 |
|
||||
| | `get_company_info` | 解析公司并列出可用 SEC filings、研报、电话会 transcript 和财务字段 |
|
||||
| | `get_company_report_text` | 获取 SEC filing、第三方研报或财报电话会 transcript 文本 |
|
||||
| | `get_crypto_holdings` | 查询机构或地址的钱包可见链上持仓证据 |
|
||||
| | `get_crypto_holding_changes` | 查询相邻快照之间的链上持仓变化 |
|
||||
| 实时 | `subscribe_latest_news` | WebSocket 实时推送,支持币种和引擎类型过滤 |
|
||||
|
||||
> 完整使用指南和详细示例,请查看 [知识指南](../knowledge/guide.md)。
|
||||
|
||||
@@ -142,6 +142,15 @@ AI-powered prediction and correlation signals:
|
||||
- **get_high_score_news**: Articles with high AI impact scores (0-100 scale)
|
||||
- **get_news_by_signal**: Filter by AI trading signal — "long" (bullish), "short" (bearish), or "neutral"
|
||||
|
||||
### Finance Enhancement
|
||||
- **search_companies**: Search public company candidates by keyword, ticker, CIK, or fuzzy company name
|
||||
- **get_company_info**: Resolve a company and list available SEC filings, research reports, earnings-call transcripts, report forms, and financial item names
|
||||
- **get_company_report_text**: Fetch SEC filing, third-party research report, or earnings-call transcript text by report name, form, accession, source key, quarter hint, section, or text search
|
||||
- **get_crypto_holdings**: Query wallet-visible on-chain holdings evidence for an institution or address; results are Blockscout address-balance evidence, not proof of beneficial ownership or a trading signal
|
||||
- **get_crypto_holding_changes**: Query on-chain holdings changes between adjacent snapshots, with optional token, date, and change-type filters
|
||||
|
||||
For the MCP tool, pass `change_types` as a comma-separated string such as `"increase,decrease"`. `get_crypto_holding_changes` defaults `auto_collect` to false; call `get_crypto_holdings` first with `auto_collect=true` or `force_collect=true` when you need to populate a fresh wallet snapshot.
|
||||
|
||||
### Real-time
|
||||
- **subscribe_latest_news**: WebSocket live feed with optional filters by coins, engine types
|
||||
|
||||
@@ -157,6 +166,8 @@ AI-powered prediction and correlation signals:
|
||||
8. **Bullish signals**: `get_news_by_signal(signal="long")` — AI-detected bullish catalysts
|
||||
9. **Multi-filter power search**: `search_news_advanced(coins="BTC,ETH", engine_types="news:Bloomberg,Reuters;market:", keyword="ETF")` — Bloomberg & Reuters ETF news for BTC/ETH plus all market signals
|
||||
10. **Live monitoring**: `subscribe_latest_news(wait_seconds=15, coins="BTC", engine_types="news:;market:")` — real-time BTC news + market anomalies
|
||||
11. **Company filings workflow**: `get_company_info(company="IBM")`, then `get_company_report_text(company="IBM", report_name="10-K", form_type="10-K")` — discover and fetch company reports
|
||||
12. **On-chain holdings evidence**: `get_crypto_holdings(address="0x...", chain="ethereum", token_symbol="ETH")` — inspect visible wallet-balance evidence
|
||||
|
||||
## Data Structure
|
||||
|
||||
|
||||
@@ -127,6 +127,66 @@ Important: You need to understand the user's query intent and perform word segme
|
||||
|
||||
---
|
||||
|
||||
## Finance Enhancement Endpoints
|
||||
|
||||
These authenticated endpoints provide company report discovery, company report text, and wallet-visible on-chain holdings evidence. Responses are wrapped as `{"success": true, "data": ...}` and may include usage quota metadata.
|
||||
|
||||
### Company Search
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/finance-enhance/company-search" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"keyword": "IBM", "limit": 20, "auto_collect": true}'
|
||||
```
|
||||
|
||||
### Company Info
|
||||
|
||||
Returns company identity, available SEC filings, third-party research reports, earnings-call transcripts, report forms, and financial item names.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/finance-enhance/company-info" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"company": "IBM", "auto_collect": true, "filing_limit": 300, "fact_limit": 5000}'
|
||||
```
|
||||
|
||||
### Company Report Text
|
||||
|
||||
Fetches SEC filing, third-party research report, or earnings-call transcript text by report name, form, accession, source key, quarter hint, section, or text search.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/finance-enhance/company-report-text" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"company": "IBM", "report_name": "10-K", "form_type": "10-K", "auto_collect": true}'
|
||||
```
|
||||
|
||||
### Crypto Holdings Evidence
|
||||
|
||||
Returns Blockscout address-balance evidence for an institution or wallet address. It is not proof of economic ownership, beneficial ownership, investment advice, or a trading signal.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/finance-enhance/crypto-holdings" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"address": "0x...", "chain": "ethereum", "token_symbol": "ETH", "limit": 100}'
|
||||
```
|
||||
|
||||
### Crypto Holding Changes
|
||||
|
||||
Returns changes between adjacent wallet snapshots. At least two snapshots are needed to produce changed records.
|
||||
The raw HTTP API accepts `change_types` as a JSON array. The MCP tool `get_crypto_holding_changes` accepts a comma-separated string such as `increase,decrease` and defaults `auto_collect` to `false`; collect holdings first when a fresh wallet snapshot is needed.
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://ai.6551.io/open/finance-enhance/crypto-holding-changes" \
|
||||
-H "Authorization: Bearer $OPENNEWS_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"address": "0x...", "chain": "ethereum", "change_types": ["increase", "decrease"], "auto_collect": false, "limit": 100}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structures
|
||||
|
||||
### News Article
|
||||
|
||||
@@ -160,6 +160,168 @@ class NewsAPIClient:
|
||||
resp = await self._request("POST", f"{self.base_url}/open/news_search", json=body)
|
||||
return resp.json()
|
||||
|
||||
async def search_companies(
|
||||
self,
|
||||
keyword: str,
|
||||
limit: int = 20,
|
||||
auto_collect: bool = True,
|
||||
) -> dict:
|
||||
"""POST /open/finance-enhance/company-search — search public company candidates."""
|
||||
body = {
|
||||
"keyword": keyword,
|
||||
"limit": limit,
|
||||
"auto_collect": auto_collect,
|
||||
}
|
||||
resp = await self._request(
|
||||
"POST",
|
||||
f"{self.base_url}/open/finance-enhance/company-search",
|
||||
json=body,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
async def get_company_info(
|
||||
self,
|
||||
company: str,
|
||||
auto_collect: bool = True,
|
||||
filing_limit: int = 300,
|
||||
fact_limit: int = 5000,
|
||||
include_all_filings: bool = False,
|
||||
) -> dict:
|
||||
"""POST /open/finance-enhance/company-info — get company metadata and report indexes."""
|
||||
body = {
|
||||
"company": company,
|
||||
"auto_collect": auto_collect,
|
||||
"filing_limit": filing_limit,
|
||||
"fact_limit": fact_limit,
|
||||
"include_all_filings": include_all_filings,
|
||||
}
|
||||
resp = await self._request(
|
||||
"POST",
|
||||
f"{self.base_url}/open/finance-enhance/company-info",
|
||||
json=body,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
async def get_company_report_text(
|
||||
self,
|
||||
company: str,
|
||||
report_name: str,
|
||||
accession: Optional[str] = None,
|
||||
form_type: Optional[str] = None,
|
||||
section_key: Optional[str] = None,
|
||||
text_search: Optional[str] = None,
|
||||
auto_collect: bool = True,
|
||||
max_section_chars: int = 100000,
|
||||
filing_search_limit: int = 300,
|
||||
section_limit: int = 1000,
|
||||
) -> dict:
|
||||
"""POST /open/finance-enhance/company-report-text — get filing/report/transcript text."""
|
||||
body: dict[str, Any] = {
|
||||
"company": company,
|
||||
"report_name": report_name,
|
||||
"auto_collect": auto_collect,
|
||||
"max_section_chars": max_section_chars,
|
||||
"filing_search_limit": filing_search_limit,
|
||||
"section_limit": section_limit,
|
||||
}
|
||||
optional_fields = {
|
||||
"accession": accession,
|
||||
"form_type": form_type,
|
||||
"section_key": section_key,
|
||||
"text_search": text_search,
|
||||
}
|
||||
body.update({key: value for key, value in optional_fields.items() if value})
|
||||
resp = await self._request(
|
||||
"POST",
|
||||
f"{self.base_url}/open/finance-enhance/company-report-text",
|
||||
json=body,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
async def get_crypto_holdings(
|
||||
self,
|
||||
institution: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
chain: Optional[str] = None,
|
||||
token_symbol: Optional[str] = None,
|
||||
token_address: Optional[str] = None,
|
||||
as_of: Optional[str] = None,
|
||||
auto_collect: bool = True,
|
||||
force_collect: bool = False,
|
||||
collect_max_rows: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""POST /open/finance-enhance/crypto-holdings — get on-chain holdings evidence."""
|
||||
body: dict[str, Any] = {
|
||||
"auto_collect": auto_collect,
|
||||
"force_collect": force_collect,
|
||||
"limit": limit,
|
||||
}
|
||||
optional_fields = {
|
||||
"institution": institution,
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"token_symbol": token_symbol,
|
||||
"token_address": token_address,
|
||||
"as_of": as_of,
|
||||
"collect_max_rows": collect_max_rows,
|
||||
}
|
||||
body.update({
|
||||
key: value for key, value in optional_fields.items()
|
||||
if value is not None and value != ""
|
||||
})
|
||||
resp = await self._request(
|
||||
"POST",
|
||||
f"{self.base_url}/open/finance-enhance/crypto-holdings",
|
||||
json=body,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
async def get_crypto_holding_changes(
|
||||
self,
|
||||
institution: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
chain: Optional[str] = None,
|
||||
token_symbol: Optional[str] = None,
|
||||
token_address: Optional[str] = None,
|
||||
change_types: Optional[list[str]] = None,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
include_unchanged: bool = False,
|
||||
auto_collect: bool = False,
|
||||
force_collect: bool = False,
|
||||
collect_max_rows: Optional[int] = None,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""POST /open/finance-enhance/crypto-holding-changes — get holdings changes."""
|
||||
body: dict[str, Any] = {
|
||||
"include_unchanged": include_unchanged,
|
||||
"auto_collect": auto_collect,
|
||||
"force_collect": force_collect,
|
||||
"limit": limit,
|
||||
}
|
||||
optional_fields = {
|
||||
"institution": institution,
|
||||
"address": address,
|
||||
"chain": chain,
|
||||
"token_symbol": token_symbol,
|
||||
"token_address": token_address,
|
||||
"change_types": change_types,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"collect_max_rows": collect_max_rows,
|
||||
}
|
||||
body.update({
|
||||
key: value for key, value in optional_fields.items()
|
||||
if value is not None and value != "" and value != []
|
||||
})
|
||||
resp = await self._request(
|
||||
"POST",
|
||||
f"{self.base_url}/open/finance-enhance/crypto-holding-changes",
|
||||
json=body,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
|
||||
|
||||
class NewsWSClient:
|
||||
|
||||
@@ -4,6 +4,7 @@ Each module is self-contained and atomic — no inter-tool dependencies.
|
||||
"""
|
||||
|
||||
from opennews_mcp.tools import discovery # noqa: F401
|
||||
from opennews_mcp.tools import finance # noqa: F401
|
||||
from opennews_mcp.tools import free # noqa: F401
|
||||
from opennews_mcp.tools import news # noqa: F401
|
||||
from opennews_mcp.tools import realtime # noqa: F401
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Finance enhancement tools — company reports and on-chain holdings evidence."""
|
||||
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from opennews_mcp.app import mcp
|
||||
from opennews_mcp.config import make_serializable, require_token
|
||||
|
||||
|
||||
def _clamp(value: int, low: int, high: int) -> int:
|
||||
return min(max(low, value), high)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def search_companies(keyword: str, ctx: Context, limit: int = 20, auto_collect: bool = True) -> dict:
|
||||
"""Search public company candidates by keyword, ticker, CIK, or fuzzy company name.
|
||||
|
||||
Args:
|
||||
keyword: Search keyword, ticker, CIK, or company name (e.g. "IBM", "Apple").
|
||||
limit: Maximum company candidates to return (default 20, max 100).
|
||||
auto_collect: Whether to trigger backend collection and retry when cache is missing.
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = _clamp(limit, 1, 100)
|
||||
try:
|
||||
result = await api.search_companies(
|
||||
keyword=keyword,
|
||||
limit=limit,
|
||||
auto_collect=auto_collect,
|
||||
)
|
||||
return make_serializable(result)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_company_info(
|
||||
company: str,
|
||||
ctx: Context,
|
||||
auto_collect: bool = True,
|
||||
filing_limit: int = 300,
|
||||
fact_limit: int = 5000,
|
||||
include_all_filings: bool = False,
|
||||
) -> dict:
|
||||
"""Resolve a company and return available SEC filings, research reports, transcripts, and financial item indexes.
|
||||
|
||||
Args:
|
||||
company: Company name, ticker, or CIK (e.g. "IBM", "AAPL", "0000320193").
|
||||
auto_collect: Whether to trigger backend collection and retry when cache is missing.
|
||||
filing_limit: Number of filings to scan (default 300, max 1000).
|
||||
fact_limit: Number of financial facts to scan (default 5000, max 20000).
|
||||
include_all_filings: Whether to return all filing forms instead of common report forms only.
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
filing_limit = _clamp(filing_limit, 1, 1000)
|
||||
fact_limit = _clamp(fact_limit, 1, 20000)
|
||||
try:
|
||||
result = await api.get_company_info(
|
||||
company=company,
|
||||
auto_collect=auto_collect,
|
||||
filing_limit=filing_limit,
|
||||
fact_limit=fact_limit,
|
||||
include_all_filings=include_all_filings,
|
||||
)
|
||||
return make_serializable(result)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_company_report_text(
|
||||
company: str,
|
||||
report_name: str,
|
||||
ctx: Context,
|
||||
accession: str = "",
|
||||
form_type: str = "",
|
||||
section_key: str = "",
|
||||
text_search: str = "",
|
||||
auto_collect: bool = True,
|
||||
max_section_chars: int = 100000,
|
||||
filing_search_limit: int = 300,
|
||||
section_limit: int = 1000,
|
||||
) -> dict:
|
||||
"""Get SEC filing, research report, or earnings-call transcript text by report name or identifier.
|
||||
|
||||
Args:
|
||||
company: Company name, ticker, or CIK.
|
||||
report_name: Report name, form, accession, research slug, transcript source key, or quarter hint.
|
||||
accession: Optional SEC accession, research-report identifier, or transcript identifier.
|
||||
form_type: Optional SEC form type such as "10-K", "10-Q", or "8-K".
|
||||
section_key: Optional section key to fetch a specific section.
|
||||
text_search: Optional keyword to search within report text.
|
||||
auto_collect: Whether to trigger backend collection and retry when cache is missing.
|
||||
max_section_chars: Maximum characters per section (default 100000).
|
||||
filing_search_limit: Candidate filing search limit (default 300).
|
||||
section_limit: Maximum returned sections (default 1000).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
max_section_chars = _clamp(max_section_chars, 1, 500000)
|
||||
filing_search_limit = _clamp(filing_search_limit, 1, 1000)
|
||||
section_limit = _clamp(section_limit, 1, 5000)
|
||||
try:
|
||||
result = await api.get_company_report_text(
|
||||
company=company,
|
||||
report_name=report_name,
|
||||
accession=accession or None,
|
||||
form_type=form_type or None,
|
||||
section_key=section_key or None,
|
||||
text_search=text_search or None,
|
||||
auto_collect=auto_collect,
|
||||
max_section_chars=max_section_chars,
|
||||
filing_search_limit=filing_search_limit,
|
||||
section_limit=section_limit,
|
||||
)
|
||||
return make_serializable(result)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_crypto_holdings(
|
||||
ctx: Context,
|
||||
institution: str = "",
|
||||
address: str = "",
|
||||
chain: str = "",
|
||||
token_symbol: str = "",
|
||||
token_address: str = "",
|
||||
as_of: str = "",
|
||||
auto_collect: bool = True,
|
||||
force_collect: bool = False,
|
||||
collect_max_rows: int = 0,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""Query visible on-chain holdings evidence for an institution or wallet address.
|
||||
|
||||
Returns Blockscout address-balance evidence. This is not proof of economic ownership,
|
||||
beneficial ownership, investment advice, or a trading signal.
|
||||
|
||||
Args:
|
||||
institution: Optional institution name for watchlist matching or result labeling.
|
||||
address: Optional wallet address. If address is provided without chain, backend defaults to ethereum.
|
||||
chain: Optional chain name.
|
||||
token_symbol: Optional token symbol filter, such as "ETH" or "USDC".
|
||||
token_address: Optional token contract address filter.
|
||||
as_of: Optional date or snapshot-time filter.
|
||||
auto_collect: Whether to collect address balances when cache is missing.
|
||||
force_collect: Whether to force a fresh address-balance collection.
|
||||
collect_max_rows: Optional collection row cap; 0 means backend default.
|
||||
limit: Maximum holdings rows to return (default 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = _clamp(limit, 1, 1000)
|
||||
collect_rows = _clamp(collect_max_rows, 1, 10000) if collect_max_rows > 0 else None
|
||||
try:
|
||||
result = await api.get_crypto_holdings(
|
||||
institution=institution or None,
|
||||
address=address or None,
|
||||
chain=chain or None,
|
||||
token_symbol=token_symbol or None,
|
||||
token_address=token_address or None,
|
||||
as_of=as_of or None,
|
||||
auto_collect=auto_collect,
|
||||
force_collect=force_collect,
|
||||
collect_max_rows=collect_rows,
|
||||
limit=limit,
|
||||
)
|
||||
return make_serializable(result)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_crypto_holding_changes(
|
||||
ctx: Context,
|
||||
institution: str = "",
|
||||
address: str = "",
|
||||
chain: str = "",
|
||||
token_symbol: str = "",
|
||||
token_address: str = "",
|
||||
change_types: str = "",
|
||||
start_date: str = "",
|
||||
end_date: str = "",
|
||||
include_unchanged: bool = False,
|
||||
auto_collect: bool = False,
|
||||
force_collect: bool = False,
|
||||
collect_max_rows: int = 0,
|
||||
limit: int = 100,
|
||||
) -> dict:
|
||||
"""Query on-chain holdings changes between adjacent wallet snapshots.
|
||||
|
||||
Args:
|
||||
institution: Optional institution name.
|
||||
address: Optional wallet address. At least two snapshots are needed for changed records.
|
||||
chain: Optional chain name.
|
||||
token_symbol: Optional token symbol filter.
|
||||
token_address: Optional token contract address filter.
|
||||
change_types: Optional comma-separated filters, such as "increase,decrease".
|
||||
start_date: Optional start date filter.
|
||||
end_date: Optional end date filter.
|
||||
include_unchanged: Whether to include unchanged rows.
|
||||
auto_collect: Whether to collect address balances when cache is missing.
|
||||
Defaults to false because address-only change queries can fail in the backend
|
||||
auto-collection path; collect holdings first when a fresh snapshot is needed.
|
||||
force_collect: Whether to force a fresh address-balance collection.
|
||||
collect_max_rows: Optional collection row cap; 0 means backend default.
|
||||
limit: Maximum change rows to return (default 100).
|
||||
"""
|
||||
if (err := require_token()):
|
||||
return err
|
||||
api = ctx.request_context.lifespan_context.api
|
||||
limit = _clamp(limit, 1, 1000)
|
||||
collect_rows = _clamp(collect_max_rows, 1, 10000) if collect_max_rows > 0 else None
|
||||
change_type_list = [item.strip() for item in change_types.split(",") if item.strip()] or None
|
||||
try:
|
||||
result = await api.get_crypto_holding_changes(
|
||||
institution=institution or None,
|
||||
address=address or None,
|
||||
chain=chain or None,
|
||||
token_symbol=token_symbol or None,
|
||||
token_address=token_address or None,
|
||||
change_types=change_type_list,
|
||||
start_date=start_date or None,
|
||||
end_date=end_date or None,
|
||||
include_unchanged=include_unchanged,
|
||||
auto_collect=auto_collect,
|
||||
force_collect=force_collect,
|
||||
collect_max_rows=collect_rows,
|
||||
limit=limit,
|
||||
)
|
||||
return make_serializable(result)
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e) or repr(e)}
|
||||
Reference in New Issue
Block a user