major agent refactoring: wiki knowledge base, no RAG, no Qdrant, no Ollama
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Research Script API Usage
|
||||
|
||||
Research scripts executed via the `execute_research` MCP tool have access to the global API instance, which provides both data fetching and charting capabilities.
|
||||
Research scripts executed via the `ExecuteResearch` MCP tool have access to the global API instance, which provides both data fetching and charting capabilities.
|
||||
|
||||
## Accessing the API
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ async def activate_strategy(
|
||||
Activate a strategy for live or paper forward trading.
|
||||
|
||||
Args:
|
||||
strategy_name: Display name as saved via python_write("strategy", ...)
|
||||
strategy_name: Display name as saved via PythonWrite("strategy", ...)
|
||||
feeds: List of feed dicts: [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600}]
|
||||
allocation: Capital allocated in quote currency (e.g. 5000.0 USDT)
|
||||
paper: True = paper/simulated fills (default); False = live (not yet implemented)
|
||||
|
||||
@@ -30,7 +30,7 @@ async def backtest_strategy(
|
||||
Load a saved strategy, fetch OHLC+ data for each feed, and run a backtest.
|
||||
|
||||
Args:
|
||||
strategy_name: Display name as saved via python_write("strategy", ...)
|
||||
strategy_name: Display name as saved via PythonWrite("strategy", ...)
|
||||
feeds: List of feed dicts, e.g. [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600}]
|
||||
from_time: Backtest start (Unix timestamp or date string)
|
||||
to_time: Backtest end (Unix timestamp or date string)
|
||||
|
||||
@@ -139,7 +139,7 @@ async def evaluate_indicator(
|
||||
"error": (
|
||||
f"Custom indicator '{pandas_ta_name}' not found after registering "
|
||||
"custom indicators. Make sure the indicator was created with "
|
||||
"python_write(category='indicator', name='...') and that its "
|
||||
"PythonWrite(category='indicator', name='...') and that its "
|
||||
"implementation.py defines a function matching the sanitized name."
|
||||
)
|
||||
}))]
|
||||
|
||||
@@ -18,6 +18,7 @@ After write/edit operations, a category-specific test harness runs to validate
|
||||
the code and capture errors/output for agent feedback.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -62,7 +63,6 @@ class BaseMetadata:
|
||||
"""Base metadata for all categories."""
|
||||
name: str # Display name (can have special chars)
|
||||
description: str # LLM-generated description
|
||||
details: str = "" # Full markdown description with enough detail to reproduce the code
|
||||
conda_packages: list[str] = None # Additional conda packages required
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -165,7 +165,12 @@ class IndicatorMetadata(BaseMetadata):
|
||||
@dataclass
|
||||
class ResearchMetadata(BaseMetadata):
|
||||
"""Metadata for research scripts."""
|
||||
pass
|
||||
output: dict = None # Output files: {"analysis": "analysis.md", "images": ["img1.png", ...]}
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.output is None:
|
||||
self.output = {}
|
||||
|
||||
|
||||
# Metadata class registry
|
||||
@@ -546,11 +551,19 @@ class CategoryFileManager:
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to write implementation: {e}"}
|
||||
|
||||
# Build metadata
|
||||
# Write details.md (stored separately from metadata)
|
||||
details_path = item_dir / "details.md"
|
||||
try:
|
||||
details_path.write_text(details or "")
|
||||
log.info(f"Wrote details: {details_path}")
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to write details: {e}"}
|
||||
|
||||
# Build metadata (details stored separately in details.md)
|
||||
meta_dict = metadata or {}
|
||||
meta_dict["name"] = name
|
||||
meta_dict["description"] = description
|
||||
meta_dict["details"] = details
|
||||
meta_dict.pop("details", None) # ensure details not stored in metadata
|
||||
|
||||
# For indicators, store the canonical pandas_ta_name so the reverse
|
||||
# mapping (ta_name → directory) is reliable regardless of name casing.
|
||||
@@ -583,7 +596,7 @@ class CategoryFileManager:
|
||||
if validation["success"]:
|
||||
if cat == Category.RESEARCH:
|
||||
log.info(f"Auto-executing research script: {name}")
|
||||
result["execution"] = await self.execute_research(name)
|
||||
result["execution"] = await self.execute_research(name, commit=False)
|
||||
elif cat == Category.INDICATOR:
|
||||
log.info(f"Auto-executing indicator test: {name}")
|
||||
result["execution"] = await self._execute_indicator(item_dir)
|
||||
@@ -652,6 +665,18 @@ class CategoryFileManager:
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to read existing metadata: {e}"}
|
||||
|
||||
# Load existing details from details.md; migrate from metadata.json if needed
|
||||
details_path = item_dir / "details.md"
|
||||
existing_details = ""
|
||||
if details_path.exists():
|
||||
existing_details = details_path.read_text()
|
||||
elif existing_meta.get("details"):
|
||||
existing_details = existing_meta.pop("details")
|
||||
try:
|
||||
details_path.write_text(existing_details)
|
||||
except Exception:
|
||||
pass # migration failure is non-fatal
|
||||
|
||||
# Apply string-replacement patches if provided
|
||||
if patches is not None:
|
||||
if not impl_path.exists():
|
||||
@@ -682,7 +707,7 @@ class CategoryFileManager:
|
||||
|
||||
# Apply text-replacement patches to details field if provided
|
||||
if detail_patches is not None:
|
||||
current_details = existing_meta.get("details", "")
|
||||
current_details = existing_details
|
||||
for i, patch in enumerate(detail_patches):
|
||||
old = patch.get("old_string", "")
|
||||
new = patch.get("new_string", "")
|
||||
@@ -693,14 +718,22 @@ class CategoryFileManager:
|
||||
current_details = current_details.replace(old, new, 1)
|
||||
details = current_details
|
||||
|
||||
# Update metadata
|
||||
# Write details.md if details was updated
|
||||
if details is not None:
|
||||
try:
|
||||
details_path.write_text(details)
|
||||
log.info(f"Updated details.md: {details_path}")
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to write details: {e}"}
|
||||
|
||||
# Update metadata (details always stored in details.md, never in metadata)
|
||||
updated_meta = existing_meta.copy()
|
||||
updated_meta.pop("details", None)
|
||||
if description is not None:
|
||||
updated_meta["description"] = description
|
||||
if details is not None:
|
||||
updated_meta["details"] = details
|
||||
if metadata:
|
||||
updated_meta.update(metadata)
|
||||
meta_updates = {k: v for k, v in metadata.items() if k != "details"}
|
||||
updated_meta.update(meta_updates)
|
||||
|
||||
# Validate and write metadata
|
||||
try:
|
||||
@@ -730,7 +763,7 @@ class CategoryFileManager:
|
||||
if code is not None and result["success"]:
|
||||
if cat == Category.RESEARCH:
|
||||
log.info(f"Auto-executing research script after edit: {name}")
|
||||
result["execution"] = await self.execute_research(name)
|
||||
result["execution"] = await self.execute_research(name, commit=False)
|
||||
elif cat == Category.INDICATOR:
|
||||
log.info(f"Auto-executing indicator test after edit: {name}")
|
||||
result["execution"] = await self._execute_indicator(item_dir)
|
||||
@@ -778,9 +811,24 @@ class CategoryFileManager:
|
||||
if meta_path.exists():
|
||||
metadata = json.loads(meta_path.read_text())
|
||||
|
||||
# Read details from details.md; migrate from metadata.json if needed
|
||||
details_path = item_dir / "details.md"
|
||||
details = ""
|
||||
if details_path.exists():
|
||||
details = details_path.read_text()
|
||||
elif metadata.get("details"):
|
||||
details = metadata.pop("details")
|
||||
try:
|
||||
details_path.write_text(details)
|
||||
meta_path.write_text(json.dumps(metadata, indent=2))
|
||||
log.info(f"Migrated details to details.md for {item_dir.name}")
|
||||
except Exception:
|
||||
pass # migration failure is non-fatal
|
||||
|
||||
return {
|
||||
"exists": True,
|
||||
"code": code,
|
||||
"details": details,
|
||||
"metadata": metadata,
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -972,12 +1020,14 @@ class CategoryFileManager:
|
||||
"images": data["images"],
|
||||
}
|
||||
|
||||
async def execute_research(self, name: str) -> dict[str, Any]:
|
||||
async def execute_research(self, name: str, commit: bool = True) -> dict[str, Any]:
|
||||
"""
|
||||
Execute a research script and return structured content with images.
|
||||
|
||||
Args:
|
||||
name: Display name of the research script
|
||||
commit: Whether to commit output files to git (default True; set False
|
||||
when called from write()/edit() which commit everything together)
|
||||
|
||||
Returns:
|
||||
dict with:
|
||||
@@ -1040,10 +1090,139 @@ class CategoryFileManager:
|
||||
log.error(f"execute_research '{name}': script failed with no output")
|
||||
return {"error": "Research script execution failed"}
|
||||
|
||||
# Persist output to output/ subdir
|
||||
if content:
|
||||
output_dir = item_dir / "output"
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
output_meta: dict[str, Any] = {}
|
||||
|
||||
if data.get("stdout"):
|
||||
analysis_path = output_dir / "analysis.md"
|
||||
try:
|
||||
analysis_path.write_text(data["stdout"])
|
||||
output_meta["analysis"] = "analysis.md"
|
||||
except Exception as e:
|
||||
log.warning(f"execute_research '{name}': failed to write analysis.md: {e}")
|
||||
|
||||
image_files = []
|
||||
for i, img in enumerate(data.get("images", []), 1):
|
||||
img_filename = f"img{i}.png"
|
||||
img_path = output_dir / img_filename
|
||||
try:
|
||||
img_path.write_bytes(base64.b64decode(img["data"]))
|
||||
image_files.append(img_filename)
|
||||
except Exception as e:
|
||||
log.warning(f"execute_research '{name}': failed to write {img_filename}: {e}")
|
||||
|
||||
if image_files:
|
||||
output_meta["images"] = image_files
|
||||
|
||||
# Update metadata.json with output section
|
||||
meta_path = item_dir / "metadata.json"
|
||||
if meta_path.exists():
|
||||
try:
|
||||
meta = json.loads(meta_path.read_text())
|
||||
meta["output"] = output_meta
|
||||
meta_path.write_text(json.dumps(meta, indent=2))
|
||||
except Exception as e:
|
||||
log.warning(f"execute_research '{name}': failed to update metadata output: {e}")
|
||||
|
||||
# Commit output files
|
||||
if commit:
|
||||
try:
|
||||
await self.git.commit_async(f"output(research): {name}")
|
||||
except Exception as e:
|
||||
log.warning(f"execute_research '{name}': git commit failed: {e}")
|
||||
|
||||
log.info(f"execute_research '{name}': returning {len(content)} content items")
|
||||
return {"content": content}
|
||||
|
||||
|
||||
def read_output(
|
||||
self,
|
||||
category: str,
|
||||
name: str,
|
||||
files: Optional[list[str]] = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Read output files for a category item.
|
||||
|
||||
Args:
|
||||
category: Category name
|
||||
name: Display name of the item
|
||||
files: Specific filenames under output/ to return (e.g. ["analysis.md", "img1.png"]).
|
||||
If omitted, returns all output files listed in metadata.
|
||||
|
||||
Returns:
|
||||
dict with:
|
||||
- content: list of TextContent and ImageContent objects (MCP format)
|
||||
- files_returned: list of filenames returned
|
||||
- output_dir: str path to output directory
|
||||
- error: str (if any)
|
||||
"""
|
||||
try:
|
||||
cat = Category(category)
|
||||
except ValueError:
|
||||
return {"error": f"Invalid category '{category}'"}
|
||||
|
||||
item_dir = get_category_path(self.src_dir, cat, name)
|
||||
if not item_dir.exists():
|
||||
return {"error": f"Item '{name}' not found in '{category}'"}
|
||||
|
||||
output_dir = item_dir / "output"
|
||||
if not output_dir.exists():
|
||||
return {"error": f"No output directory for '{name}' — run the script first"}
|
||||
|
||||
# Determine which files to return
|
||||
if files is None:
|
||||
meta_path = item_dir / "metadata.json"
|
||||
if meta_path.exists():
|
||||
try:
|
||||
meta = json.loads(meta_path.read_text())
|
||||
output_meta = meta.get("output") or {}
|
||||
files = []
|
||||
if output_meta.get("analysis"):
|
||||
files.append(output_meta["analysis"])
|
||||
files.extend(output_meta.get("images") or [])
|
||||
except Exception:
|
||||
files = []
|
||||
if not files:
|
||||
# Fallback: return all files in output dir
|
||||
files = [p.name for p in sorted(output_dir.iterdir()) if p.is_file()]
|
||||
|
||||
if not files:
|
||||
return {"error": f"No output files found for '{name}'"}
|
||||
|
||||
from mcp.types import TextContent, ImageContent
|
||||
|
||||
content = []
|
||||
files_returned = []
|
||||
|
||||
for filename in files:
|
||||
file_path = output_dir / filename
|
||||
if not file_path.exists():
|
||||
log.warning(f"Output file not found: {file_path}")
|
||||
continue
|
||||
|
||||
suffix = file_path.suffix.lower()
|
||||
if suffix in ('.md', '.txt'):
|
||||
text = file_path.read_text()
|
||||
content.append(TextContent(type="text", text=text))
|
||||
files_returned.append(filename)
|
||||
elif suffix in ('.png', '.jpg', '.jpeg'):
|
||||
data = base64.b64encode(file_path.read_bytes()).decode()
|
||||
mime = "image/png" if suffix == '.png' else "image/jpeg"
|
||||
content.append(ImageContent(type="image", data=data, mimeType=mime))
|
||||
files_returned.append(filename)
|
||||
else:
|
||||
log.warning(f"Unsupported output file type: {filename}")
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"files_returned": files_returned,
|
||||
"output_dir": str(output_dir),
|
||||
}
|
||||
|
||||
async def delete(self, category: str, name: str) -> dict[str, Any]:
|
||||
"""
|
||||
Delete a category script directory and commit the removal to git.
|
||||
|
||||
@@ -7,9 +7,9 @@ in the user container. These stores sync with the gateway and web client.
|
||||
Storage location: {DATA_DIR}/workspace/{store_name}.json
|
||||
|
||||
Available tools:
|
||||
- workspace_read(store_name) -> dict
|
||||
- workspace_write(store_name, data) -> None
|
||||
- workspace_patch(store_name, patch) -> dict
|
||||
- WorkspaceRead(store_name) -> dict
|
||||
- WorkspaceWrite(store_name, data) -> None
|
||||
- WorkspacePatch(store_name, patch) -> dict
|
||||
|
||||
Future: Path-based triggers for container-side reactions to state changes.
|
||||
"""
|
||||
@@ -322,14 +322,14 @@ def register_workspace_tools(server):
|
||||
@server.call_tool()
|
||||
async def handle_tool_call(name: str, arguments: dict) -> Any:
|
||||
"""Handle workspace tool calls."""
|
||||
if name == "workspace_read":
|
||||
if name == "WorkspaceRead":
|
||||
return store.read(arguments.get("store_name", ""))
|
||||
elif name == "workspace_write":
|
||||
elif name == "WorkspaceWrite":
|
||||
return store.write(
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("data")
|
||||
)
|
||||
elif name == "workspace_patch":
|
||||
elif name == "WorkspacePatch":
|
||||
return store.patch(
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("patch", [])
|
||||
@@ -342,7 +342,7 @@ def register_workspace_tools(server):
|
||||
"""List available workspace tools."""
|
||||
return [
|
||||
{
|
||||
"name": "workspace_read",
|
||||
"name": "WorkspaceRead",
|
||||
"description": "Read a workspace store from persistent storage",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
@@ -356,7 +356,7 @@ def register_workspace_tools(server):
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workspace_write",
|
||||
"name": "WorkspaceWrite",
|
||||
"description": "Write a workspace store to persistent storage",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
@@ -373,7 +373,7 @@ def register_workspace_tools(server):
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "workspace_patch",
|
||||
"name": "WorkspacePatch",
|
||||
"description": "Apply JSON patch operations to a workspace store",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
|
||||
230
sandbox/main.py
230
sandbox/main.py
@@ -158,7 +158,7 @@ def _remove_indicator_instances(workspace_store, pandas_ta_name: str) -> None:
|
||||
def _workspace_sync_content(workspace_store, category: str) -> "TextContent | None":
|
||||
"""
|
||||
Return a TextContent item carrying the current {category}_types workspace state so the
|
||||
gateway can sync it to connected web clients without a separate workspace_patch call.
|
||||
gateway can sync it to connected web clients without a separate WorkspacePatch call.
|
||||
The gateway detects items of the form {"_workspace_sync": {"store": ..., "data": ...}}.
|
||||
"""
|
||||
store = _type_store_name(category)
|
||||
@@ -304,7 +304,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
@server.list_resources()
|
||||
async def list_resources():
|
||||
"""List available resources"""
|
||||
return [
|
||||
resources = [
|
||||
{
|
||||
"uri": f"dexorder://user/{config.user_id}/hello",
|
||||
"name": "Hello World",
|
||||
@@ -312,6 +312,14 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"mimeType": "text/plain",
|
||||
}
|
||||
]
|
||||
if _get_env_yml() is not None:
|
||||
resources.append({
|
||||
"uri": f"dexorder://user/{config.user_id}/environment.yml",
|
||||
"name": "Conda Environment",
|
||||
"description": "Base conda environment packages available in all scripts",
|
||||
"mimeType": "text/yaml",
|
||||
})
|
||||
return resources
|
||||
|
||||
@server.read_resource()
|
||||
async def read_resource(uri: str):
|
||||
@@ -332,6 +340,15 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"mimeType": "text/plain",
|
||||
"text": f"Hello from Dexorder user container!\nUser ID: {config.user_id}\n",
|
||||
}
|
||||
elif uri == f"dexorder://user/{config.user_id}/environment.yml":
|
||||
env_yml = _get_env_yml()
|
||||
if env_yml is None:
|
||||
raise ValueError("environment.yml not found")
|
||||
return {
|
||||
"uri": uri,
|
||||
"mimeType": "text/yaml",
|
||||
"text": env_yml.read_text(encoding="utf-8"),
|
||||
}
|
||||
else:
|
||||
raise ValueError(f"Unknown resource: {uri}")
|
||||
|
||||
@@ -340,7 +357,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"""List available tools including workspace and category tools"""
|
||||
return [
|
||||
Tool(
|
||||
name="workspace_read",
|
||||
name="WorkspaceRead",
|
||||
description="Read a workspace store from persistent storage",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -354,7 +371,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="workspace_write",
|
||||
name="WorkspaceWrite",
|
||||
description="Write a workspace store to persistent storage",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -371,7 +388,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="workspace_patch",
|
||||
name="WorkspacePatch",
|
||||
description="Apply JSON patch operations to a workspace store",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -398,7 +415,47 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_write",
|
||||
name="PreferencesRead",
|
||||
description="Read the user preferences markdown file. Returns the full content of preferences.md from the user's sandbox data directory.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="PreferencesWrite",
|
||||
description="Write (fully replace) the user preferences markdown file. Use this to create or overwrite preferences.md with new content.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Full markdown content for the preferences file"
|
||||
}
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="PreferencesPatch",
|
||||
description="Surgically update a section of the user preferences markdown file by finding and replacing text. Fails if old_str is not found.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"old_str": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find in the preferences file"
|
||||
},
|
||||
"new_str": {
|
||||
"type": "string",
|
||||
"description": "Replacement text"
|
||||
}
|
||||
},
|
||||
"required": ["old_str", "new_str"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="PythonWrite",
|
||||
description="Write a new strategy, indicator, or research script with validation",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -422,7 +479,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"Full markdown description of the code with sufficient detail that another coding agent "
|
||||
"could functionally reproduce the implementation from this field alone. "
|
||||
"Include: purpose, algorithm, all parameters and their semantics, data feed usage, "
|
||||
"formulas, edge cases, and any non-obvious implementation choices (required)."
|
||||
"formulas, edge cases, and any non-obvious implementation choices (required). "
|
||||
"Stored as a separate details.md file alongside the implementation."
|
||||
)
|
||||
},
|
||||
"code": {
|
||||
@@ -446,7 +504,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_edit",
|
||||
name="PythonEdit",
|
||||
description=(
|
||||
"Edit an existing category script. "
|
||||
"Use 'patches' for targeted string replacements (preferred for small changes), "
|
||||
@@ -525,8 +583,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_read",
|
||||
description="Read a category script and its metadata",
|
||||
name="PythonRead",
|
||||
description="Read a category script, its metadata, and details. Returns: code, details (markdown), and metadata.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -544,7 +602,38 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_list",
|
||||
name="PythonReadOutput",
|
||||
description=(
|
||||
"Read persisted output files from a previous research script execution. "
|
||||
"Returns TextContent for .md/.txt files and ImageContent for images. "
|
||||
"Output is saved automatically when ExecuteResearch or PythonWrite/PythonEdit runs a research script."
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["strategy", "indicator", "research"],
|
||||
"description": "Category of the script"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the item"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": (
|
||||
"Specific filenames under output/ to return (e.g. [\"analysis.md\", \"img1.png\"]). "
|
||||
"If omitted, returns all output files listed in metadata."
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": ["category", "name"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="PythonList",
|
||||
description="List all items in a category with names and descriptions",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -559,7 +648,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_log",
|
||||
name="PythonLog",
|
||||
description="Show git commit history for category items. Filter by category and/or name to see history for a specific item.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -583,7 +672,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_revert",
|
||||
name="PythonRevert",
|
||||
description="Restore a category item to a previous git revision. Creates a new commit — non-destructive.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -606,7 +695,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="python_delete",
|
||||
name="PythonDelete",
|
||||
description="Delete a category script permanently. Commits removal to git history and removes any conda packages that are no longer needed.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -625,7 +714,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="conda_sync",
|
||||
name="CondaSync",
|
||||
description="Sync conda packages: scan all metadata, remove unused packages (excluding base environment)",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -634,7 +723,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="conda_install",
|
||||
name="CondaInstall",
|
||||
description="Install conda packages on-demand",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -649,7 +738,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="execute_research",
|
||||
name="ExecuteResearch",
|
||||
description="Execute a research script and return results with matplotlib images",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -663,7 +752,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="evaluate_indicator",
|
||||
name="EvaluateIndicator",
|
||||
description=(
|
||||
"Evaluate a pandas-ta indicator against real OHLC data and return a structured "
|
||||
"array of timestamped values. Use this to validate that an indicator computes "
|
||||
@@ -701,7 +790,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="backtest_strategy",
|
||||
name="BacktestStrategy",
|
||||
description=(
|
||||
"Run a saved trading strategy against historical OHLC data using Nautilus Trader "
|
||||
"BacktestEngine. Returns performance metrics (total return, Sharpe ratio, "
|
||||
@@ -714,7 +803,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"properties": {
|
||||
"strategy_name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the strategy as saved via python_write"
|
||||
"description": "Display name of the strategy as saved via PythonWrite"
|
||||
},
|
||||
"feeds": {
|
||||
"type": "array",
|
||||
@@ -757,7 +846,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="activate_strategy",
|
||||
name="ActivateStrategy",
|
||||
description=(
|
||||
"Activate a strategy for paper or live forward trading with a capital allocation. "
|
||||
"paper=true (default): simulated fills on live data — no API keys required. "
|
||||
@@ -769,7 +858,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"properties": {
|
||||
"strategy_name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the strategy as saved via python_write"
|
||||
"description": "Display name of the strategy as saved via PythonWrite"
|
||||
},
|
||||
"feeds": {
|
||||
"type": "array",
|
||||
@@ -798,7 +887,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="deactivate_strategy",
|
||||
name="DeactivateStrategy",
|
||||
description="Stop an active strategy and return its final P&L summary.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -812,7 +901,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="list_active_strategies",
|
||||
name="ListActiveStrategies",
|
||||
description="List all currently active (live or paper) strategies and their status.",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
@@ -821,7 +910,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_backtest_results",
|
||||
name="GetBacktestResults",
|
||||
description=(
|
||||
"Retrieve stored backtest results for a strategy. "
|
||||
"Returns the most recent backtest runs with summary stats, "
|
||||
@@ -844,7 +933,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_strategy_trades",
|
||||
name="GetStrategyTrades",
|
||||
description=(
|
||||
"Retrieve the trade log for a strategy (live/paper or backtest). "
|
||||
"Returns individual round-trip trades with entry/exit prices and PnL."
|
||||
@@ -866,7 +955,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
name="get_strategy_events",
|
||||
name="GetStrategyEvents",
|
||||
description=(
|
||||
"Retrieve the event log for a strategy "
|
||||
"(PnL updates, fills, errors, status changes)."
|
||||
@@ -905,19 +994,38 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
raise
|
||||
|
||||
async def _handle_tool_call_inner(name: str, arguments: dict):
|
||||
if name == "workspace_read":
|
||||
if name == "WorkspaceRead":
|
||||
return workspace_store.read(arguments.get("store_name", ""))
|
||||
elif name == "workspace_write":
|
||||
elif name == "WorkspaceWrite":
|
||||
return workspace_store.write(
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("data")
|
||||
)
|
||||
elif name == "workspace_patch":
|
||||
elif name == "WorkspacePatch":
|
||||
return workspace_store.patch(
|
||||
arguments.get("store_name", ""),
|
||||
arguments.get("patch", [])
|
||||
)
|
||||
elif name == "python_write":
|
||||
elif name == "PreferencesRead":
|
||||
prefs_path = DATA_DIR / "preferences.md"
|
||||
if not prefs_path.exists():
|
||||
return {"content": "", "exists": False}
|
||||
content = prefs_path.read_text(encoding="utf-8")
|
||||
return {"content": content, "exists": True}
|
||||
elif name == "PreferencesWrite":
|
||||
prefs_path = DATA_DIR / "preferences.md"
|
||||
prefs_path.write_text(arguments.get("content", ""), encoding="utf-8")
|
||||
return {"success": True}
|
||||
elif name == "PreferencesPatch":
|
||||
prefs_path = DATA_DIR / "preferences.md"
|
||||
old_str = arguments.get("old_str", "")
|
||||
new_str = arguments.get("new_str", "")
|
||||
content = prefs_path.read_text(encoding="utf-8") if prefs_path.exists() else ""
|
||||
if old_str not in content:
|
||||
return {"success": False, "error": "old_str not found in preferences file"}
|
||||
prefs_path.write_text(content.replace(old_str, new_str, 1), encoding="utf-8")
|
||||
return {"success": True}
|
||||
elif name == "PythonWrite":
|
||||
result = await category_manager.write(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
@@ -941,9 +1049,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
exec_content = result["execution"].get("content", [])
|
||||
content.extend(exec_content)
|
||||
image_count = sum(1 for item in exec_content if item.type == "image")
|
||||
logging.info(f"python_write '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
logging.info(f"PythonWrite '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
else:
|
||||
logging.info(f"python_write '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
|
||||
logging.info(f"PythonWrite '{arguments.get('name')}': no execution result (category={arguments.get('category')})")
|
||||
if result.get("success"):
|
||||
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
||||
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
||||
@@ -951,7 +1059,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
if sync:
|
||||
content.append(sync)
|
||||
return content
|
||||
elif name == "python_edit":
|
||||
elif name == "PythonEdit":
|
||||
result = await category_manager.edit(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
@@ -977,9 +1085,9 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
exec_content = result["execution"].get("content", [])
|
||||
content.extend(exec_content)
|
||||
image_count = sum(1 for item in exec_content if item.type == "image")
|
||||
logging.info(f"python_edit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
logging.info(f"PythonEdit '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
else:
|
||||
logging.info(f"python_edit '{arguments.get('name')}': no execution result")
|
||||
logging.info(f"PythonEdit '{arguments.get('name')}': no execution result")
|
||||
if result.get("success"):
|
||||
_upsert_type(workspace_store, category_manager, arguments.get("category", ""), arguments.get("name", ""))
|
||||
await cleanup_extra_packages_async(get_data_dir(), _get_env_yml())
|
||||
@@ -987,16 +1095,30 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
if sync:
|
||||
content.append(sync)
|
||||
return content
|
||||
elif name == "python_read":
|
||||
elif name == "PythonRead":
|
||||
return category_manager.read(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", "")
|
||||
)
|
||||
elif name == "python_list":
|
||||
elif name == "PythonReadOutput":
|
||||
result = category_manager.read_output(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
files=arguments.get("files"),
|
||||
)
|
||||
if "error" in result:
|
||||
return [TextContent(type="text", text=f"Error: {result['error']}")]
|
||||
content = result.get("content", [])
|
||||
summary = TextContent(
|
||||
type="text",
|
||||
text=f"output_dir: {result.get('output_dir', '')}\nfiles_returned: {result.get('files_returned', [])}"
|
||||
)
|
||||
return [summary] + content
|
||||
elif name == "PythonList":
|
||||
return category_manager.list_items(
|
||||
category=arguments.get("category", "")
|
||||
)
|
||||
elif name == "python_log":
|
||||
elif name == "PythonLog":
|
||||
result = await category_manager.git_log(
|
||||
category=arguments.get("category"),
|
||||
name=arguments.get("name"),
|
||||
@@ -1006,7 +1128,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
for c in result.get("commits", []):
|
||||
lines.append(f"{c['short_hash']} {c['date'][:10]} {c['message']}")
|
||||
return [TextContent(type="text", text="\n".join(lines))]
|
||||
elif name == "python_revert":
|
||||
elif name == "PythonRevert":
|
||||
result = await category_manager.git_revert(
|
||||
revision=arguments.get("revision", ""),
|
||||
category=arguments.get("category", ""),
|
||||
@@ -1027,7 +1149,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
content_out.append(sync)
|
||||
return content_out
|
||||
return [TextContent(type="text", text="\n".join(meta_parts))]
|
||||
elif name == "python_delete":
|
||||
elif name == "PythonDelete":
|
||||
result = await category_manager.delete(
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", "")
|
||||
@@ -1047,23 +1169,23 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
if sync:
|
||||
content_out.append(sync)
|
||||
return content_out
|
||||
elif name == "conda_sync":
|
||||
elif name == "CondaSync":
|
||||
return await sync_packages_async(
|
||||
data_dir=get_data_dir(),
|
||||
environment_yml=_get_env_yml()
|
||||
)
|
||||
elif name == "conda_install":
|
||||
elif name == "CondaInstall":
|
||||
return await install_packages_async(arguments.get("packages", []))
|
||||
elif name == "execute_research":
|
||||
elif name == "ExecuteResearch":
|
||||
result = await category_manager.execute_research(name=arguments.get("name", ""))
|
||||
if "error" in result:
|
||||
logging.error(f"execute_research '{arguments.get('name')}': {result['error']}")
|
||||
logging.error(f"ExecuteResearch '{arguments.get('name')}': {result['error']}")
|
||||
return [TextContent(type="text", text=f"Error: {result['error']}")]
|
||||
content = result.get("content", [TextContent(type="text", text="No output")])
|
||||
image_count = sum(1 for item in content if item.type == "image")
|
||||
logging.info(f"execute_research '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
logging.info(f"ExecuteResearch '{arguments.get('name')}': returning {len(content)} items, {image_count} images")
|
||||
return content
|
||||
elif name == "evaluate_indicator":
|
||||
elif name == "EvaluateIndicator":
|
||||
return await evaluate_indicator(
|
||||
symbol=arguments.get("symbol", ""),
|
||||
from_time=arguments.get("from_time"),
|
||||
@@ -1072,7 +1194,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
pandas_ta_name=arguments.get("pandas_ta_name", ""),
|
||||
parameters=arguments.get("parameters") or {},
|
||||
)
|
||||
elif name == "backtest_strategy":
|
||||
elif name == "BacktestStrategy":
|
||||
result = await backtest_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
feeds=arguments.get("feeds", []),
|
||||
@@ -1101,20 +1223,20 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
except Exception as _e:
|
||||
logging.debug("Failed to persist backtest results: %s", _e)
|
||||
return result
|
||||
elif name == "activate_strategy":
|
||||
elif name == "ActivateStrategy":
|
||||
return await activate_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
feeds=arguments.get("feeds", []),
|
||||
allocation=float(arguments.get("allocation", 0.0)),
|
||||
paper=bool(arguments.get("paper", True)),
|
||||
)
|
||||
elif name == "deactivate_strategy":
|
||||
elif name == "DeactivateStrategy":
|
||||
return await deactivate_strategy(
|
||||
strategy_name=arguments.get("strategy_name", ""),
|
||||
)
|
||||
elif name == "list_active_strategies":
|
||||
elif name == "ListActiveStrategies":
|
||||
return await list_active_strategies()
|
||||
elif name == "get_backtest_results":
|
||||
elif name == "GetBacktestResults":
|
||||
from dexorder.strategy.db import get_strategy_db
|
||||
db = get_strategy_db(get_data_dir())
|
||||
results = await db.get_backtests(
|
||||
@@ -1122,7 +1244,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
limit=int(arguments.get("limit", 5)),
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps({"backtest_runs": results}))]
|
||||
elif name == "get_strategy_trades":
|
||||
elif name == "GetStrategyTrades":
|
||||
from dexorder.strategy.db import get_strategy_db
|
||||
db = get_strategy_db(get_data_dir())
|
||||
trades = await db.get_trades(
|
||||
@@ -1130,7 +1252,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
limit=int(arguments.get("limit", 100)),
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps({"trades": trades}))]
|
||||
elif name == "get_strategy_events":
|
||||
elif name == "GetStrategyEvents":
|
||||
from dexorder.strategy.db import get_strategy_db
|
||||
db = get_strategy_db(get_data_dir())
|
||||
events = await db.get_events(
|
||||
|
||||
Reference in New Issue
Block a user