Merge PR #1647: structured reads of manifest-listed run artifacts

This commit is contained in:
Haozhe Wu committed 2026-10-04 02:33:57 +08:00
commit 085131f81e
11 files changed
+1230 -7

No files matched your search

+1 -1
View File
@@ -1644,7 +1644,7 @@ with `--host` / `--port`.
</details>
**MCP tools exposed (75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**MCP tools exposed (76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### SWARM external MCP tools
+1 -1
View File
@@ -1324,7 +1324,7 @@ vibe-trading-mcp --transport sse # legacy SSE (deprecated)
</details>
**أدوات MCP المعروضة (75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**أدوات MCP المعروضة (76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### أدوات MCP الخارجية في SWARM
+1 -1
View File
@@ -1399,7 +1399,7 @@ dirección de bind con `--host` / `--port`.
</details>
**MCP tools expuestas (75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**MCP tools expuestas (76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### SWARM external MCP tools
+1 -1
View File
@@ -1643,7 +1643,7 @@ dengan `--host` / `--port`.
</details>
**Tool MCP yang diekspos (75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**Tool MCP yang diekspos (76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### Tool MCP eksternal untuk SWARM
+1 -1
View File
@@ -1326,7 +1326,7 @@ vibe-trading-mcp --transport sse # legacy SSE (deprecated)
</details>
**公開される MCP tools(75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**公開される MCP tools(76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### SWARM の外部 MCP tools
+1 -1
View File
@@ -1327,7 +1327,7 @@ vibe-trading-mcp --transport sse # legacy SSE (deprecated)
</details>
**노출되는 MCP tools(75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**노출되는 MCP tools(76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### SWARM 외부 MCP tools
+1 -1
View File
@@ -1331,7 +1331,7 @@ vibe-trading-mcp --transport sse # legacy SSE (deprecated)
</details>
**暴露的 MCP tools(75):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
**暴露的 MCP tools(76):** `list_skills`, `load_skill`, `start_research_goal`, `get_research_goal`, `add_goal_evidence`, `update_research_goal_status`, `backtest`, `factor_analysis`, `alpha_zoo`, `alpha_bench`, `analyze_options`, `analyze_options_payoff`, `pattern_recognition`, `read_url`, `read_document`, `web_search`, `write_file`, `read_file`, `read_run_artifact`, `list_strategies`, `query_strategies`, `get_strategy_evidence`, `refresh_strategy_evidence`, `list_swarm_presets`, `run_swarm`, `get_market_data`, `get_fund_flow`, `get_dragon_tiger`, `get_northbound_flow`, `get_southbound_flow`, `get_margin_trading`, `get_block_trades`, `get_shareholder_count`, `get_lockup_expiry`, `get_sector_info`, `get_research_reports`, `get_stock_news`, `get_sec_filings`, `get_financial_statements`, `get_options_chain`, `get_stock_profile`, `screen_market`, `search_symbol`, `get_macro_series`, `iwencai_search`, `qveris_search`, `qveris_inspect`, `qveris_execute`, `get_institutional_holdings`, `etf_holdings`, `prediction_market`, `research_papers`, `get_swarm_status`, `get_run_result`, `list_runs`, `reap_stale_runs`, `retry_run`, `analyze_trade_journal`, `extract_shadow_strategy`, `run_shadow_backtest`, `render_shadow_report`, `scan_shadow_signals`, `trading_connections`, `trading_select_connection`, `trading_check`, `trading_account`, `trading_positions`, `trading_orders`, `trading_quote`, `trading_history`, `quantlib_call`, `cashflow_performance`, `orderbook_depth`, `sentiment`, `technical_indicators`, `get_fundamentals`.
### SWARM 的外部 MCP tools
+1
View File
@@ -178,6 +178,7 @@ Use `load_skill(name)` to access full methodology docs with code templates.
| `read_document` | Extract text from PDF/DOCX/XLSX/PPTX/images | None |
| `write_file` | Write files (config, strategy code) | None |
| `read_file` | Read file contents | None |
| `read_run_artifact` | Structured run-artifact reads (rows / downsample / meta) | None |
| `list_strategies` | Browse discoverable strategies (Alpha Zoo + SDM store) | None |
| `query_strategies` | Evidence-gated query: regime / Sharpe / quality / cost filters | None |
| `get_strategy_evidence` | Per-regime evidence rows for one strategy | None |
+59
View File
@@ -1191,6 +1191,65 @@ def read_file(path: str) -> str:
return registry.execute("read_file", {"path": path})
@mcp.tool
def read_run_artifact(
run_dir: str,
artifact: str,
format: str = "rows",
offset: int = 0,
max_rows: int = 1000,
columns: _lenient_str_list_opt = None,
) -> str:
"""Read a backtest run artifact as structured JSON instead of raw CSV text.
Artifacts: friendly aliases ``equity``, ``trades``, ``metrics``,
``positions``, ``target_positions`` (CSVs under ``<run_dir>/artifacts/``),
``ohlcv:<CODE>`` (e.g. ``ohlcv:600519.SH``) and ``run_card`` — plus any
run_dir-relative ``.csv``/``.json`` path listed in the run's
``run_card.json`` artifacts manifest (e.g. ``artifacts/validation.json``).
The manifest is the trust anchor: a relative path it does not list is
refused, and a manifest entry pointing outside the run directory is never
followed.
Formats:
- ``rows``: offset paging over whole records; follow ``next_offset`` until
``truncated`` is false to walk a file losslessly.
- ``downsample``: equal-stride sample of at most ``max_rows`` points with
the first and last row always pinned — one call feeds a chart.
- ``meta``: columns / total_rows / size_bytes, plus the manifest's
``sha256`` and ``manifest_size_bytes`` for a manifest-listed artifact.
Cells arrive typed (int / float / null / string). Every envelope is
serialized within a bounded byte budget (~120K characters): an oversized
page shrinks to whole records with honest ``truncated`` / ``next_offset``
metadata rather than cutting mid-JSON. Errors return
``{"ok": false, "error", "hint"}``.
Args:
run_dir: Run directory a backtest/tool call returned.
artifact: Alias name or manifest-listed relative path (see above);
anything else — including path traversal — is refused.
format: "rows" (default), "downsample" or "meta".
offset: First row index for "rows" mode (default 0).
max_rows: Page/sample size, clamped to [1, 5000] (default 1000).
columns: Optional column projection; unknown names are refused with
the valid list.
"""
registry = _get_registry()
return registry.execute(
"read_run_artifact",
{
"run_dir": run_dir,
"artifact": artifact,
"format": format,
"offset": offset,
"max_rows": max_rows,
"columns": columns,
},
)
# ---------------------------------------------------------------------------
# Strategy discovery tools
# ---------------------------------------------------------------------------
+598
View File
@@ -0,0 +1,598 @@
"""Run-artifact reader: chunked, downsampled, structured JSON over run files.
A backtest run writes CSV artifacts (``equity.csv``, ``trades.csv``, ...) plus
a ``run_card.json`` sidecar whose ``artifacts`` manifest lists every file the
run produced with its sha256 and size. The generic ``read_file`` tool returns
raw text, burning LLM tokens on comma-separated noise and truncating mid-file.
This tool parses the artifact once and serves structured JSON pages:
* ``rows`` — offset paging over whole records with an honest ``truncated`` /
``next_offset`` contract, so a walk reassembles the file losslessly.
* ``downsample`` — equal-stride sampling to at most ``max_rows`` points, first
and last row always pinned; ``offset`` is ignored (the sample spans all).
* ``meta`` — columns / total_rows / size_bytes, plus the manifest's ``sha256``
and ``manifest_size_bytes`` for a manifest-listed artifact.
Resolution is manifest-driven: friendly aliases (``equity``, ``ohlcv:<CODE>``,
``run_card``) resolve by a fixed mapping, and any other value is treated as a
run_dir-relative path accepted ONLY when the run_card manifest lists it — a
new artifact kind becomes readable with zero code change, while a tampered
card can never point the reader outside the run directory (every entry is
validated, every resolution re-checked inside ``run_root``). Serialized
envelopes stay under :data:`_BYTE_BUDGET` by shrinking whole records (never
mid-record, never broken JSON), following ``src/tools/_result_paging.py``.
"""
from __future__ import annotations
import csv
import json
import math
import re
from pathlib import Path
from typing import Any
from src.agent.tools import BaseTool
from src.tools.path_utils import safe_run_dir
# Serialized-envelope budget in characters. MCP clients truncate large tool
# results (50KB is a common default; raised harness limits reach 256KB); 120K
# keeps a full page intact under the raised limit while leaving framing room.
_BYTE_BUDGET = 120_000
# Friendly CSV aliases: name -> path relative to run_dir.
_CSV_ARTIFACTS: dict[str, str] = {
"equity": "artifacts/equity.csv",
"trades": "artifacts/trades.csv",
"metrics": "artifacts/metrics.csv",
"positions": "artifacts/positions.csv",
"target_positions": "artifacts/target_positions.csv",
}
_RUN_CARD_ALIAS = "run_card"
_RUN_CARD_PATH = "run_card.json"
_OHLCV_PREFIX = "ohlcv:"
_VALID_FORMATS = ("rows", "downsample", "meta")
_MAX_ROWS_CEILING = 5000
_SERVED_SUFFIXES = (".csv", ".json")
_WINDOWS_DRIVE = re.compile(r"^[A-Za-z]:")
_ALIAS_HINT = (
"artifact must be one of: "
+ ", ".join([*_CSV_ARTIFACTS, f"{_OHLCV_PREFIX}<CODE>", _RUN_CARD_ALIAS])
+ ", or a run_dir-relative .csv/.json path listed in the run_card.json "
"artifacts manifest (e.g. artifacts/validation.json)."
)
_MANIFEST_HINT = (
"Pass a friendly alias, or a run_dir-relative path the run's run_card.json "
"artifacts manifest lists; a run without a readable card only serves aliases."
)
class _ArtifactError(ValueError):
"""Artifact-resolution failure carrying its own user-facing hint."""
def __init__(self, error: str, hint: str) -> None:
super().__init__(error)
self.hint = hint
def _error(error: str, hint: str) -> str:
"""Serialize the error envelope: ``{"ok": false, "error", "hint"}``."""
return json.dumps({"ok": False, "error": error, "hint": hint}, ensure_ascii=False)
def _validate_ohlcv_code(code: str) -> None:
"""Reject an ``ohlcv:<CODE>`` suffix that could escape the artifacts dir.
Raises:
ValueError: If the code is empty or carries path separators, parent
references, a leading dot, or a null byte.
"""
if not code or not code.strip():
raise ValueError("ohlcv: requires a non-empty symbol code")
if "/" in code or "\\" in code or "\x00" in code:
raise ValueError(f"ohlcv code {code!r} must not contain path separators")
if code.startswith("."):
raise ValueError(f"ohlcv code {code!r} must not start with a dot")
if ".." in code:
raise ValueError(f"ohlcv code {code!r} must not contain '..'")
def _safe_relative_resolve(run_root: Path, relative: str) -> Path | None:
"""Resolve a manifest-relative path, or return ``None`` if it is unsafe.
Rejects empty paths, null bytes, absolute paths (POSIX or Windows
shaped), any ``..`` segment under either separator style, and anything
whose resolution leaves ``run_root`` (e.g. through a symlink) — so a
tampered manifest entry is never followed.
"""
if not relative or "\x00" in relative:
return None
if relative.startswith(("/", "\\")) or _WINDOWS_DRIVE.match(relative):
return None
if any(segment == ".." for segment in relative.replace("\\", "/").split("/")):
return None
if Path(relative).is_absolute():
return None
resolved = (run_root / relative).resolve()
return resolved if resolved.is_relative_to(run_root) else None
def _load_artifact_manifest(run_root: Path) -> dict[str, tuple[Path, dict[str, Any]]]:
"""Load and validate the ``run_card.json`` artifacts manifest.
The manifest is the trust anchor for non-alias artifact paths: an entry is
followed only after its own validation (see :func:`_safe_relative_resolve`),
so a missing, corrupt, or tampered card degrades to "no manifest" — aliases
keep working against the disk, and relative paths are refused.
Returns:
Mapping of manifest path string to ``(resolved_path, entry)``; empty
when the card carries no well-formed ``artifacts`` list.
"""
try:
card = json.loads((run_root / _RUN_CARD_PATH).read_text(encoding="utf-8"))
except (OSError, ValueError, UnicodeDecodeError):
return {}
entries = card.get("artifacts") if isinstance(card, dict) else None
if not isinstance(entries, list):
return {}
manifest: dict[str, tuple[Path, dict[str, Any]]] = {}
for entry in entries:
if not isinstance(entry, dict):
continue
relative = entry.get("path")
if not isinstance(relative, str):
continue
resolved = _safe_relative_resolve(run_root, relative)
if resolved is not None:
manifest[relative] = (resolved, entry)
return manifest
def _resolve_artifact(
run_root: Path,
artifact: str,
manifest: dict[str, tuple[Path, dict[str, Any]]],
) -> tuple[Path, dict[str, Any] | None]:
"""Map an alias or manifest-listed path to a file inside ``run_root``.
Friendly aliases resolve by a fixed mapping and need no card; any other
value is accepted only when the validated manifest lists it and it is a
``.csv``/``.json`` file. Every branch re-verifies containment.
Returns:
``(resolved_path, manifest_entry)``; the entry is ``None`` when the
artifact is not manifest-listed.
Raises:
_ArtifactError: If the name is neither a valid alias nor a
manifest-listed served-suffix path, or it escapes ``run_root``.
"""
if artifact in _CSV_ARTIFACTS:
relative = _CSV_ARTIFACTS[artifact]
elif artifact == _RUN_CARD_ALIAS:
relative = _RUN_CARD_PATH
elif artifact.startswith(_OHLCV_PREFIX):
code = artifact[len(_OHLCV_PREFIX) :]
try:
_validate_ohlcv_code(code)
except ValueError as exc:
raise _ArtifactError(str(exc), _ALIAS_HINT) from exc
relative = f"artifacts/ohlcv_{code}.csv"
else:
listed = manifest.get(artifact)
if listed is None:
raise _ArtifactError(
f"artifact {artifact!r} is not a known alias and is not "
"listed in the run_card.json artifacts manifest",
_MANIFEST_HINT,
)
path, entry = listed
if path.suffix.lower() not in _SERVED_SUFFIXES:
raise _ArtifactError(
f"manifest artifact {artifact!r} is not a .csv or .json file",
"read_run_artifact serves CSV and JSON artifacts; use "
"read_file for other files.",
)
return path, entry
resolved = (run_root / relative).resolve()
if not resolved.is_relative_to(run_root):
raise _ArtifactError(
f"artifact {artifact!r} resolves outside run_dir", _ALIAS_HINT
)
listed = manifest.get(relative)
return resolved, listed[1] if listed else None
def _coerce_value(raw: str) -> Any:
"""Coerce one CSV cell to int / float / None / str.
Integer-looking text becomes ``int``, finite float-looking text becomes
``float``, empty or non-finite values (``nan``/``inf`` in any spelling
``float()`` accepts) become ``None``, anything else stays a string.
Underscored numerics (``1_000``) stay strings because ``int()``/``float()``
would silently accept them.
"""
text = raw.strip()
if not text:
return None
if "_" not in text:
try:
return int(text)
except ValueError:
pass
try:
value = float(text)
except ValueError:
return raw
return value if math.isfinite(value) else None
return raw
def _read_csv(path: Path) -> tuple[list[str], list[list[Any]]]:
"""Parse a CSV artifact into columns plus coerced, header-aligned rows.
Each row has exactly ``len(columns)`` cells (short rows padded with
``None``, extra trailing cells dropped); an empty file yields ``([], [])``.
"""
with path.open("r", encoding="utf-8-sig", newline="") as handle:
reader = csv.reader(handle)
try:
header = next(reader)
except StopIteration:
return [], []
width = len(header)
rows: list[list[Any]] = []
for record in reader:
if not record:
continue
aligned = record[:width] + [""] * (width - len(record))
rows.append([_coerce_value(cell) for cell in aligned])
return header, rows
def _project(
columns: list[str], rows: list[list[Any]], requested: list[str]
) -> tuple[list[str], list[list[Any]]]:
"""Project rows onto the requested columns, preserving request order.
Raises:
ValueError: If a requested name is not in ``columns``; the message
lists the valid columns.
"""
unknown = [name for name in requested if name not in columns]
if unknown:
raise ValueError(f"unknown column(s) {unknown}; valid columns: {columns}")
indices = [columns.index(name) for name in requested]
return list(requested), [[row[i] for i in indices] for row in rows]
def _downsample_indices(total: int, max_points: int) -> tuple[list[int], int]:
"""Pick equal-stride sample indices with the first and last row pinned.
When ``total <= max_points`` every index is returned with stride 1.
"""
if total <= 0:
return [], 1
if max_points >= total:
return list(range(total)), 1
if max_points < 2:
# Both endpoints outrank the point cap: a one-point "sample" could
# never pin first AND last, so the floor is two.
return [0, total - 1], max(1, total - 1)
stride = -(-(total - 1) // (max_points - 1)) # ceil division
indices = list(range(0, total, stride))
if indices[-1] != total - 1:
indices.append(total - 1)
return indices, stride
def _serialize(envelope: dict[str, Any]) -> str:
"""Serialize an envelope the way every response in this tool does."""
return json.dumps(envelope, ensure_ascii=False)
def _fit_rows_payload(
base: dict[str, Any],
page: list[list[Any]],
offset: int,
total_rows: int,
budget: int,
) -> str:
"""Serialize a rows-mode page, shrinking whole rows until it fits budget.
Mirrors ``_result_paging.fit_records``: shrink proportionally to the
overflow (then one further) so wide records still converge, keeping
``returned_rows`` / ``truncated`` / ``next_offset`` honest — a shrunk page
always reports ``truncated: true`` with the resume offset, so the tail is
never dropped silently. The payload stays valid JSON even when a single
row alone exceeds the budget.
"""
count = len(page)
while True:
rows = page[:count]
returned = len(rows)
truncated = offset + returned < total_rows
envelope = {
**base,
"rows": rows,
"returned_rows": returned,
"truncated": truncated,
"next_offset": offset + returned if truncated else None,
}
payload = _serialize(envelope)
if len(payload) <= budget or count <= 1:
return payload
count = max(1, min(count - 1, int(count * budget / len(payload))))
def _fit_downsample_payload(
base: dict[str, Any], rows: list[list[Any]], max_points: int, budget: int
) -> str:
"""Serialize a downsample envelope, re-striding to fewer points if needed.
``base`` is the envelope skeleton without ``rows`` / ``downsample``
fields; ``rows`` are the parsed (and projected) source rows. The first
and last source row stay pinned at every sample size.
"""
total = len(rows)
target = max_points
while True:
indices, stride = _downsample_indices(total, target)
sample = [rows[i] for i in indices]
envelope = {
**base,
"rows": sample,
"returned_rows": len(sample),
"truncated": False,
"next_offset": None,
"downsample": {
"stride": stride,
"pinned_last": True,
"algorithm": "every-nth",
"total_rows": total,
},
}
payload = _serialize(envelope)
if len(payload) <= budget or target <= 2:
return payload
target = max(2, min(target - 1, int(target * budget / len(payload))))
def _read_json_artifact(path: Path, artifact: str, run_dir: str) -> str:
"""Parse and envelope a small JSON artifact (run_card or manifest-listed).
Returns the ``{"artifact", "run_dir", "json", "size_bytes"}`` envelope, or
the error envelope when the file is corrupt — never partial JSON.
"""
try:
parsed = json.loads(path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
return _error(
f"{artifact} is not valid JSON: {exc}",
f"Delete or regenerate {path.name}; this tool never returns partial JSON.",
)
return _serialize(
{
"artifact": artifact,
"run_dir": run_dir,
"json": parsed,
"size_bytes": path.stat().st_size,
}
)
def _meta_envelope(
base: dict[str, Any],
total_rows: int,
path: Path,
entry: dict[str, Any] | None,
) -> str:
"""Serialize a ``meta`` envelope, enriched from the manifest when listed.
A manifest-listed artifact additionally carries the card's claims when it
was written — ``sha256`` and ``manifest_size_bytes`` — beside the real
``size_bytes`` stat, so a caller can compare the run's verified-evidence
record against the file on disk.
"""
envelope: dict[str, Any] = {
**base,
"total_rows": total_rows,
"size_bytes": path.stat().st_size,
}
if entry is not None:
digest = entry.get("sha256")
if isinstance(digest, str):
envelope["sha256"] = digest
declared = entry.get("size_bytes")
if isinstance(declared, int) and not isinstance(declared, bool):
envelope["manifest_size_bytes"] = declared
return _serialize(envelope)
def read_run_artifact(
run_dir: str,
artifact: str,
format: str = "rows",
offset: int = 0,
max_rows: int = 1000,
columns: list[str] | None = None,
) -> str:
"""Read one run artifact as structured, budget-bounded JSON.
Args:
run_dir: Run directory; must sit inside an allowed run root.
artifact: Friendly alias — ``equity``/``trades``/``metrics``/
``positions``/``target_positions`` (CSVs under ``artifacts/``),
``ohlcv:<CODE>``, ``run_card`` — or any run_dir-relative
``.csv``/``.json`` path listed in the run's ``run_card.json``
artifacts manifest (e.g. ``artifacts/validation.json``).
format: ``rows`` (offset paging), ``downsample`` (equal-stride sample,
first+last pinned; ``offset`` ignored) or ``meta`` (shape only).
JSON artifacts return their parsed object regardless of format.
offset: First row index for ``rows`` mode; negative values refused.
max_rows: Page/sample size, clamped to ``[1, 5000]``.
columns: Optional projection applied before budget fitting; an unknown
name is refused with the valid column list.
Returns:
JSON string. Success envelopes carry no ``ok``/``status`` field;
failures are ``{"ok": false, "error": ..., "hint": ...}``. ``meta``
on a manifest-listed artifact additionally carries the manifest's
``sha256`` and ``manifest_size_bytes`` beside the real ``size_bytes``.
"""
if format not in _VALID_FORMATS:
return _error(
f"unknown format {format!r}",
f"format must be one of: {', '.join(_VALID_FORMATS)}.",
)
try:
run_root = safe_run_dir(run_dir)
except ValueError as exc:
return _error(str(exc), "Pass the run_dir a backtest/tool call returned.")
manifest = _load_artifact_manifest(run_root)
try:
path, entry = _resolve_artifact(run_root, artifact, manifest)
except _ArtifactError as exc:
return _error(str(exc), exc.hint)
if not path.is_file():
return _error(
f"artifact {artifact!r} not found at {path}",
"The run may not have produced it yet — check the backtest result's "
"'artifacts' map, or use format='meta' on an artifact that exists.",
)
if path.suffix.lower() == ".json":
return _read_json_artifact(path, artifact, run_dir)
try:
offset = int(offset)
max_rows = int(max_rows)
except (TypeError, ValueError):
return _error(
"offset and max_rows must be integers",
"Omit them for the defaults (offset=0, max_rows=1000).",
)
if offset < 0:
return _error(
f"offset must be >= 0, got {offset}",
"Start at offset=0 and follow next_offset.",
)
max_rows = max(1, min(max_rows, _MAX_ROWS_CEILING))
try:
header, rows = _read_csv(path)
except (OSError, UnicodeDecodeError, csv.Error) as exc:
return _error(
f"failed to read {artifact}: {exc}",
"The file may be corrupt or not a UTF-8 CSV artifact.",
)
if columns is not None:
try:
header, rows = _project(header, rows, list(columns))
except ValueError as exc:
return _error(
str(exc),
"Pass only column names from the artifact header, or omit 'columns'.",
)
total_rows = len(rows)
base: dict[str, Any] = {"artifact": artifact, "run_dir": run_dir, "columns": header}
budget = _BYTE_BUDGET
if format == "meta":
return _meta_envelope(base, total_rows, path, entry)
if format == "downsample":
return _fit_downsample_payload(
{**base, "total_rows": total_rows, "offset": 0}, rows, max_rows, budget
)
# rows mode
page = rows[offset : offset + max_rows] if offset < total_rows else []
envelope_base = {
**base,
"total_rows": total_rows,
"offset": offset,
"downsample": None,
}
return _fit_rows_payload(envelope_base, page, offset, total_rows, budget)
class RunArtifactTool(BaseTool):
"""Chunked/downsampled reader for backtest run artifacts."""
name = "read_run_artifact"
description = (
"Read a backtest run artifact as structured JSON: paged rows "
"(format='rows', follow next_offset), an equal-stride chart sample "
"with first+last pinned (format='downsample', offset ignored), or "
"shape only (format='meta', plus the manifest sha256 when listed). "
"Artifacts: equity, trades, metrics, positions, target_positions, "
"ohlcv:<CODE>, run_card, or any run_dir-relative .csv/.json path "
"listed in the run_card.json artifacts manifest. Values arrive typed "
"(int/float/null/string), never as raw CSV text, and every page stays "
"within a bounded byte budget."
)
parameters = {
"type": "object",
"properties": {
"run_dir": {"type": "string", "description": "Path to the run directory"},
"artifact": {
"type": "string",
"description": (
"Artifact alias: equity | trades | metrics | positions | "
"target_positions | ohlcv:<CODE> (e.g. ohlcv:600519.SH) | "
"run_card — or a run_dir-relative path listed in the "
"run_card.json artifacts manifest (e.g. "
"artifacts/validation.json)"
),
},
"format": {
"type": "string",
"enum": list(_VALID_FORMATS),
"description": (
"rows: offset paging over whole records; downsample: "
"equal-stride sample with first+last row pinned (offset "
"ignored); meta: columns/total_rows/size_bytes only"
),
},
"offset": {
"type": "integer",
"description": "First row index (rows mode only, default 0)",
},
"max_rows": {
"type": "integer",
"description": "Page/sample size, clamped to [1, 5000] (default 1000)",
},
"columns": {
"type": "array",
"items": {"type": "string"},
"description": (
"Optional column projection; unknown names are refused "
"with the valid list"
),
},
},
"required": ["run_dir", "artifact"],
}
repeatable = True
is_readonly = True
def execute(self, **kwargs: Any) -> str:
"""Execute the artifact read (see :func:`read_run_artifact`)."""
return read_run_artifact(
run_dir=kwargs["run_dir"],
artifact=kwargs["artifact"],
format=kwargs.get("format", "rows"),
offset=kwargs.get("offset", 0),
max_rows=kwargs.get("max_rows", 1000),
columns=kwargs.get("columns"),
)
+565
View File
@@ -0,0 +1,565 @@
"""Tests for the run-artifact reader tool.
Covers the contract of ``src.tools.run_artifact_tool``: manifest-driven
resolution (friendly aliases with and without a run card, manifest-listed
relative paths, tampered-manifest refusal), traversal rejection, CSV
coercion, offset paging, first+last-pinned downsampling, meta/JSON
envelopes including the manifest sha256, column projection, and the
byte-budget shrink loop.
"""
from __future__ import annotations
import csv
import hashlib
import json
from pathlib import Path
import pytest
from src.tools import run_artifact_tool
from src.tools.run_artifact_tool import RunArtifactTool, read_run_artifact
EQUITY_ROWS = 3821
EQUITY_COLUMNS = ["date", "equity", "cash", "drawdown"]
ROWS_ENVELOPE_KEYS = {
"artifact",
"run_dir",
"columns",
"rows",
"total_rows",
"offset",
"returned_rows",
"truncated",
"downsample",
"next_offset",
}
def _expected_row(i: int) -> list:
"""Return the coerced form of equity.csv data row ``i``."""
return [f"day{i:05d}", 100000 + i * 1.5, 1000 + i, None]
def _make_run(root: Path, rows: int = EQUITY_ROWS) -> Path:
"""Create a run directory carrying a programmatically generated equity.csv."""
run_dir = root / "run"
(run_dir / "artifacts").mkdir(parents=True, exist_ok=True)
with (run_dir / "artifacts" / "equity.csv").open(
"w", newline="", encoding="utf-8"
) as handle:
writer = csv.writer(handle)
writer.writerow(EQUITY_COLUMNS)
for i in range(rows):
writer.writerow([f"day{i:05d}", 100000 + i * 1.5, 1000 + i, ""])
return run_dir
@pytest.fixture()
def run_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Return a run directory inside a tmp_path-scoped allowed run root."""
monkeypatch.setenv("VIBE_TRADING_ALLOWED_RUN_ROOTS", str(tmp_path))
return _make_run(tmp_path)
def _write_csv(path: Path, header: list[str], rows: list[list[str]]) -> None:
"""Write a small CSV artifact."""
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="", encoding="utf-8") as handle:
writer = csv.writer(handle)
writer.writerow(header)
writer.writerows(rows)
def _manifest_entry(run_dir: Path, relative: str) -> dict:
"""Build a run_card manifest entry for a file on disk (run_card 1.0 shape)."""
path = run_dir / relative
return {
"path": relative,
"size_bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
def _write_run_card(run_dir: Path, entries: list[dict]) -> None:
"""Write a run_card.json carrying the given artifacts manifest."""
(run_dir / "run_card.json").write_text(
json.dumps({"schema_version": "1.0", "artifacts": entries}),
encoding="utf-8",
)
# --------------------------------------------------------------------------
# (a) downsample
# --------------------------------------------------------------------------
def test_downsample_caps_points_and_pins_first_last(run_dir: Path) -> None:
env = json.loads(
read_run_artifact(str(run_dir), "equity", format="downsample", max_rows=1000)
)
assert set(env) == ROWS_ENVELOPE_KEYS
assert len(env["rows"]) <= 1000
assert env["rows"][0] == _expected_row(0)
assert env["rows"][-1] == _expected_row(EQUITY_ROWS - 1)
ds = env["downsample"]
assert ds == {
"stride": 4,
"pinned_last": True,
"algorithm": "every-nth",
"total_rows": EQUITY_ROWS,
}
assert env["total_rows"] == EQUITY_ROWS
assert env["truncated"] is False
assert env["next_offset"] is None
def test_downsample_returns_everything_when_total_fits(run_dir: Path) -> None:
_write_csv(run_dir / "artifacts" / "trades.csv", ["a"], [["1"], ["2"], ["3"]])
env = json.loads(
read_run_artifact(str(run_dir), "trades", format="downsample", max_rows=100)
)
assert env["rows"] == [[1], [2], [3]]
assert env["downsample"]["stride"] == 1
assert env["downsample"]["pinned_last"] is True
# --------------------------------------------------------------------------
# (b) offset paging walk
# --------------------------------------------------------------------------
def test_offset_paging_walk_is_lossless(run_dir: Path) -> None:
gathered: list[list] = []
offset = 0
for _ in range(20): # 3821 / 500 -> 8 pages; hard stop guards a loop bug
env = json.loads(
read_run_artifact(str(run_dir), "equity", offset=offset, max_rows=500)
)
assert env["offset"] == offset
gathered.extend(env["rows"])
if not env["truncated"]:
assert env["next_offset"] is None
break
assert env["next_offset"] == offset + env["returned_rows"]
offset = env["next_offset"]
else:
pytest.fail("paging walk did not terminate")
assert len(gathered) == EQUITY_ROWS
assert gathered == [_expected_row(i) for i in range(EQUITY_ROWS)]
def test_offset_past_end_returns_empty_page(run_dir: Path) -> None:
env = json.loads(
read_run_artifact(str(run_dir), "equity", offset=EQUITY_ROWS + 100)
)
assert env["rows"] == []
assert env["returned_rows"] == 0
assert env["truncated"] is False
assert env["next_offset"] is None
def test_negative_offset_rejected(run_dir: Path) -> None:
env = json.loads(read_run_artifact(str(run_dir), "equity", offset=-1))
assert env["ok"] is False
def test_max_rows_clamped(run_dir: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(run_artifact_tool, "_BYTE_BUDGET", 10_000_000)
_write_csv(
run_dir / "artifacts" / "trades.csv", ["n"], [[str(i)] for i in range(5200)]
)
tiny = json.loads(read_run_artifact(str(run_dir), "trades", max_rows=0))
assert tiny["returned_rows"] == 1
huge = json.loads(read_run_artifact(str(run_dir), "trades", max_rows=10**6))
assert huge["returned_rows"] == 5000
assert huge["truncated"] is True
assert huge["next_offset"] == 5000
# --------------------------------------------------------------------------
# (c) traversal / alias whitelist / manifest gate
# --------------------------------------------------------------------------
@pytest.mark.parametrize(
"artifact",
[
"../../etc/passwd",
"/etc/passwd",
"ohlcv:../../x",
"ohlcv:/abs",
"ohlcv:",
"ohlcv:.hidden",
"equity.csv",
"config",
"",
],
)
def test_non_whitelisted_artifacts_rejected(run_dir: Path, artifact: str) -> None:
env = json.loads(read_run_artifact(str(run_dir), artifact))
assert env["ok"] is False
assert set(env) == {"ok", "error", "hint"}
assert env["hint"]
def test_run_dir_outside_allowed_roots_rejected(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv("VIBE_TRADING_ALLOWED_RUN_ROOTS", str(tmp_path))
env = json.loads(read_run_artifact(str(tmp_path.parent / "elsewhere"), "equity"))
assert env["ok"] is False
assert "run roots" in env["error"]
def test_symlink_escape_rejected(run_dir: Path, tmp_path: Path) -> None:
secret = tmp_path / "secret.csv"
secret.write_text("a\n1\n", encoding="utf-8")
(run_dir / "artifacts" / "trades.csv").symlink_to(secret)
env = json.loads(read_run_artifact(str(run_dir), "trades"))
assert env["ok"] is False
# --------------------------------------------------------------------------
# (c2) manifest-driven resolution
# --------------------------------------------------------------------------
def test_manifest_listed_json_artifact_resolves(run_dir: Path) -> None:
payload = {"walk_forward": {"oos_sharpe": 0.9}}
(run_dir / "artifacts" / "validation.json").write_text(
json.dumps(payload), encoding="utf-8"
)
_write_run_card(run_dir, [_manifest_entry(run_dir, "artifacts/validation.json")])
env = json.loads(read_run_artifact(str(run_dir), "artifacts/validation.json"))
assert set(env) == {"artifact", "run_dir", "json", "size_bytes"}
assert env["artifact"] == "artifacts/validation.json"
assert env["json"] == payload
def test_manifest_listed_csv_artifact_resolves(run_dir: Path) -> None:
_write_csv(
run_dir / "artifacts" / "rebalance_notes.csv",
["date", "note"],
[["2024-01-02", "held"]],
)
entry = _manifest_entry(run_dir, "artifacts/rebalance_notes.csv")
_write_run_card(run_dir, [entry])
env = json.loads(read_run_artifact(str(run_dir), "artifacts/rebalance_notes.csv"))
assert env["columns"] == ["date", "note"]
assert env["rows"] == [["2024-01-02", "held"]]
meta = json.loads(
read_run_artifact(str(run_dir), "artifacts/rebalance_notes.csv", format="meta")
)
assert meta["sha256"] == entry["sha256"]
assert meta["manifest_size_bytes"] == entry["size_bytes"]
def test_relative_path_not_in_manifest_refused(run_dir: Path) -> None:
_write_csv(run_dir / "artifacts" / "extra.csv", ["a"], [["1"]])
_write_run_card(run_dir, []) # a card exists but lists nothing
env = json.loads(read_run_artifact(str(run_dir), "artifacts/extra.csv"))
assert env["ok"] is False
assert set(env) == {"ok", "error", "hint"}
assert "manifest" in env["hint"]
@pytest.mark.parametrize("tampered", ["../evil.csv", "..\\evil.csv"])
def test_tampered_manifest_escape_entries_refused(
run_dir: Path, tmp_path: Path, tampered: str
) -> None:
(tmp_path / "evil.csv").write_text(
"a\n1\n", encoding="utf-8"
) # the escape target exists
_write_run_card(
run_dir, [{"path": tampered, "size_bytes": 4, "sha256": "deadbeef"}]
)
env = json.loads(read_run_artifact(str(run_dir), tampered))
assert env["ok"] is False
assert set(env) == {"ok", "error", "hint"}
def test_tampered_manifest_absolute_path_refused(run_dir: Path, tmp_path: Path) -> None:
secret = tmp_path / "secret.csv"
secret.write_text("a\n1\n", encoding="utf-8")
_write_run_card(
run_dir, [{"path": str(secret), "size_bytes": 4, "sha256": "deadbeef"}]
)
env = json.loads(read_run_artifact(str(run_dir), str(secret)))
assert env["ok"] is False
def test_manifest_symlink_entry_escaping_run_dir_refused(
run_dir: Path, tmp_path: Path
) -> None:
secret = tmp_path / "secret.csv"
secret.write_text("a\n1\n", encoding="utf-8")
(run_dir / "artifacts" / "link.csv").symlink_to(secret)
_write_run_card(run_dir, [_manifest_entry(run_dir, "artifacts/link.csv")])
env = json.loads(read_run_artifact(str(run_dir), "artifacts/link.csv"))
assert env["ok"] is False
def test_manifest_listed_non_served_suffix_refused(run_dir: Path) -> None:
(run_dir / "code").mkdir(parents=True, exist_ok=True)
(run_dir / "code" / "signal_engine.py").write_text("print(1)\n", encoding="utf-8")
_write_run_card(run_dir, [_manifest_entry(run_dir, "code/signal_engine.py")])
env = json.loads(read_run_artifact(str(run_dir), "code/signal_engine.py"))
assert env["ok"] is False
assert "read_file" in env["hint"]
def test_aliases_work_without_run_card(run_dir: Path) -> None:
assert not (run_dir / "run_card.json").exists()
env = json.loads(read_run_artifact(str(run_dir), "equity", format="meta"))
assert env["total_rows"] == EQUITY_ROWS
assert "sha256" not in env and "manifest_size_bytes" not in env
def test_aliases_work_with_corrupt_run_card(run_dir: Path) -> None:
(run_dir / "run_card.json").write_text("{oops not json", encoding="utf-8")
env = json.loads(read_run_artifact(str(run_dir), "equity", format="meta"))
assert env["total_rows"] == EQUITY_ROWS
refused = json.loads(read_run_artifact(str(run_dir), "artifacts/equity.csv"))
assert refused["ok"] is False
assert "manifest" in refused["hint"]
def test_meta_exposes_manifest_sha256(run_dir: Path) -> None:
entry = _manifest_entry(run_dir, "artifacts/equity.csv")
_write_run_card(run_dir, [entry])
env = json.loads(read_run_artifact(str(run_dir), "equity", format="meta"))
assert env["sha256"] == entry["sha256"]
assert env["manifest_size_bytes"] == entry["size_bytes"]
assert env["size_bytes"] == (run_dir / "artifacts" / "equity.csv").stat().st_size
# --------------------------------------------------------------------------
# (d) structured output, no raw CSV
# --------------------------------------------------------------------------
def test_rows_are_structured_arrays_not_raw_csv(run_dir: Path) -> None:
payload = read_run_artifact(str(run_dir), "equity", max_rows=50)
assert "day00000,100000.0" not in payload # no raw CSV line survives
env = json.loads(payload)
assert env["columns"] == EQUITY_COLUMNS
for row in env["rows"]:
assert isinstance(row, list)
assert all(cell is None or isinstance(cell, (int, float, str)) for cell in row)
assert all("\n" not in cell for cell in row if isinstance(cell, str))
# --------------------------------------------------------------------------
# (e) coercion
# --------------------------------------------------------------------------
def test_numeric_coercion(run_dir: Path) -> None:
_write_csv(
run_dir / "artifacts" / "metrics.csv",
["i", "f", "blank", "nan_col", "inf_col", "text"],
[
["42", "3.14", "", "nan", "inf", "hello"],
["-7", "1e5", " ", "NaN", "-inf", "1_000"],
["0", "2.0", "x", "Infinity", "-Infinity", "null"],
],
)
env = json.loads(read_run_artifact(str(run_dir), "metrics"))
assert env["rows"][0] == [42, 3.14, None, None, None, "hello"]
assert env["rows"][1] == [-7, 100000.0, None, None, None, "1_000"]
assert env["rows"][2] == [0, 2.0, "x", None, None, "null"]
# --------------------------------------------------------------------------
# (f) meta
# --------------------------------------------------------------------------
def test_meta_format(run_dir: Path) -> None:
env = json.loads(read_run_artifact(str(run_dir), "equity", format="meta"))
assert set(env) == {"artifact", "run_dir", "columns", "total_rows", "size_bytes"}
assert env["artifact"] == "equity"
assert env["columns"] == EQUITY_COLUMNS
assert env["total_rows"] == EQUITY_ROWS
assert env["size_bytes"] == (run_dir / "artifacts" / "equity.csv").stat().st_size
def test_unknown_format_rejected(run_dir: Path) -> None:
env = json.loads(read_run_artifact(str(run_dir), "equity", format="yaml"))
assert env["ok"] is False
# --------------------------------------------------------------------------
# (g) projection
# --------------------------------------------------------------------------
def test_columns_projection(run_dir: Path) -> None:
env = json.loads(
read_run_artifact(
str(run_dir), "equity", columns=["equity", "date"], max_rows=10
)
)
assert env["columns"] == ["equity", "date"]
assert env["rows"][0] == [100000.0, "day00000"]
assert all(len(row) == 2 for row in env["rows"])
def test_unknown_column_lists_valid_columns(run_dir: Path) -> None:
env = json.loads(
read_run_artifact(str(run_dir), "equity", columns=["equity", "nope"])
)
assert env["ok"] is False
assert "nope" in env["error"]
for column in EQUITY_COLUMNS:
assert column in env["error"]
# --------------------------------------------------------------------------
# (h) byte budget
# --------------------------------------------------------------------------
def test_byte_budget_shrinks_rows_page_honestly(
run_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(run_artifact_tool, "_BYTE_BUDGET", 3000)
payload = read_run_artifact(str(run_dir), "equity", max_rows=1000)
assert len(payload) <= 3000
env = json.loads(payload) # never broken JSON
assert 0 < env["returned_rows"] < 1000
assert env["returned_rows"] == len(env["rows"])
assert env["truncated"] is True
assert env["next_offset"] == env["offset"] + env["returned_rows"]
assert env["rows"][0] == _expected_row(0)
# The advertised resume point serves a valid next page.
follow = json.loads(
read_run_artifact(
str(run_dir), "equity", offset=env["next_offset"], max_rows=1000
)
)
assert follow["rows"][0] == _expected_row(env["next_offset"])
def test_byte_budget_restrides_downsample(
run_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(run_artifact_tool, "_BYTE_BUDGET", 3000)
payload = read_run_artifact(
str(run_dir), "equity", format="downsample", max_rows=1000
)
assert len(payload) <= 3000
env = json.loads(payload)
assert env["returned_rows"] < 1000
assert env["downsample"]["stride"] > 4 # re-striden, not silently truncated
assert env["downsample"]["total_rows"] == EQUITY_ROWS
assert env["rows"][0] == _expected_row(0)
assert env["rows"][-1] == _expected_row(EQUITY_ROWS - 1)
def test_byte_budget_never_breaks_json_at_floor(
run_dir: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(run_artifact_tool, "_BYTE_BUDGET", 10)
env = json.loads(read_run_artifact(str(run_dir), "equity", max_rows=100))
assert env["returned_rows"] == 1
assert env["truncated"] is True
assert env["next_offset"] == 1
# --------------------------------------------------------------------------
# (i) ohlcv
# --------------------------------------------------------------------------
def test_ohlcv_code_maps_to_artifacts_file(run_dir: Path) -> None:
_write_csv(
run_dir / "artifacts" / "ohlcv_600519.SH.csv",
["date", "open", "close"],
[["2024-01-02", "100.5", "101.25"]],
)
env = json.loads(read_run_artifact(str(run_dir), "ohlcv:600519.SH"))
assert env["artifact"] == "ohlcv:600519.SH"
assert env["columns"] == ["date", "open", "close"]
assert env["rows"] == [["2024-01-02", 100.5, 101.25]]
# --------------------------------------------------------------------------
# (j) run_card / corrupt JSON
# --------------------------------------------------------------------------
def test_run_card_returns_parsed_json_regardless_of_format(run_dir: Path) -> None:
card = {"run_id": "abc", "metrics": {"sharpe": 1.25}, "tags": ["x"]}
(run_dir / "run_card.json").write_text(json.dumps(card), encoding="utf-8")
for fmt in ("rows", "downsample", "meta"):
env = json.loads(read_run_artifact(str(run_dir), "run_card", format=fmt))
assert set(env) == {"artifact", "run_dir", "json", "size_bytes"}
assert env["json"] == card
assert env["size_bytes"] == (run_dir / "run_card.json").stat().st_size
def test_corrupt_run_card_rejected(run_dir: Path) -> None:
(run_dir / "run_card.json").write_text("{oops not json", encoding="utf-8")
env = json.loads(read_run_artifact(str(run_dir), "run_card"))
assert env["ok"] is False
assert set(env) == {"ok", "error", "hint"}
# --------------------------------------------------------------------------
# (k) missing file
# --------------------------------------------------------------------------
def test_missing_artifact_has_hint(run_dir: Path) -> None:
env = json.loads(read_run_artifact(str(run_dir), "trades"))
assert env["ok"] is False
assert env["hint"]
assert "not found" in env["error"]
def test_manifest_listed_but_missing_on_disk_has_hint(run_dir: Path) -> None:
_write_run_card(
run_dir,
[{"path": "artifacts/validation.json", "size_bytes": 2, "sha256": "deadbeef"}],
)
env = json.loads(read_run_artifact(str(run_dir), "artifacts/validation.json"))
assert env["ok"] is False
assert "not found" in env["error"]
# --------------------------------------------------------------------------
# envelope hygiene + tool class
# --------------------------------------------------------------------------
def test_success_envelope_carries_no_ok_field(run_dir: Path) -> None:
env = json.loads(read_run_artifact(str(run_dir), "equity", max_rows=5))
assert "ok" not in env and "status" not in env
assert set(env) == ROWS_ENVELOPE_KEYS
assert env["downsample"] is None
assert env["run_dir"] == str(run_dir)
def test_tool_class_contract(run_dir: Path) -> None:
tool = RunArtifactTool()
assert tool.name == "read_run_artifact"
assert tool.is_readonly is True
assert tool.repeatable is True
assert tool.description
assert "progress" not in tool.description
schema = tool.to_openai_schema()
props = schema["function"]["parameters"]["properties"]
assert props["format"]["enum"] == ["rows", "downsample", "meta"]
assert schema["function"]["parameters"]["required"] == ["run_dir", "artifact"]
meta = json.loads(
tool.execute(run_dir=str(run_dir), artifact="equity", format="meta")
)
assert meta["total_rows"] == EQUITY_ROWS
default = json.loads(
tool.execute(run_dir=str(run_dir), artifact="equity", max_rows=3)
)
assert set(default) == ROWS_ENVELOPE_KEYS
assert default["returned_rows"] == 3