subagent thinking accordion; indicator fixes; script details & edit

This commit is contained in:
2026-04-20 15:09:37 -04:00
parent a188268906
commit b1d4459809
25 changed files with 2041 additions and 174 deletions

View File

@@ -144,10 +144,25 @@ async def evaluate_indicator(
)
}))]
# Get input_series from the indicator's metadata
indicator_name = pandas_ta_name[len("custom_"):]
# Get input_series from the indicator's metadata.
# Look up by the stored pandas_ta_name field (written at creation time),
# which is the reliable reverse mapping from ta_name → directory.
# Fall back to display-name matching for indicators created before this
# field was added.
mgr = get_category_manager()
read_result = mgr.read("indicator", indicator_name)
all_items = mgr.list_items("indicator")
read_result = {"exists": False}
for item in all_items.get("items", []):
meta = item.get("metadata", {})
# Primary: match stored pandas_ta_name field (exact, set at write time)
if meta.get("pandas_ta_name") == name_lower:
read_result = mgr.read("indicator", item.get("name", ""))
break
# Fallback: infer from display name (legacy indicators without the field)
item_name = item.get("name", "")
if "custom_" + item_name.lower().replace("-", "_").replace(" ", "_") == name_lower:
read_result = mgr.read("indicator", item_name)
break
if read_result.get("exists") and read_result.get("metadata"):
raw_series = read_result["metadata"].get("input_series") or ["close"]
input_cols = tuple(raw_series)

View File

@@ -62,6 +62,12 @@ 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):
if self.conda_packages is None:
self.conda_packages = []
@dataclass
@@ -69,21 +75,21 @@ class StrategyMetadata(BaseMetadata):
"""Metadata for trading strategies."""
data_feeds: list[dict] = None # Required data feeds: [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600, "description": "..."}]
parameters: dict = None # Strategy parameters: {"param_name": {"default": value, "description": "..."}}
conda_packages: list[str] = None # Additional conda packages required
def __post_init__(self):
super().__post_init__()
if self.data_feeds is None:
self.data_feeds = []
if self.parameters is None:
self.parameters = {}
if self.conda_packages is None:
self.conda_packages = []
@dataclass
class IndicatorMetadata(BaseMetadata):
"""Metadata for technical indicators."""
conda_packages: list[str] = None # Additional conda packages required
# Canonical pandas-ta name, e.g. "custom_trendflex". Set automatically
# by CategoryFileManager.write() — do not pass manually.
pandas_ta_name: str = ""
# Fields for TradingView custom study auto-construction:
parameters: dict = None
@@ -143,8 +149,7 @@ class IndicatorMetadata(BaseMetadata):
# Example (RSI levels): [{"id": "ob", "value": 70}, {"id": "os", "value": 30}]
def __post_init__(self):
if self.conda_packages is None:
self.conda_packages = []
super().__post_init__()
if self.input_series is None:
self.input_series = ["close"]
if self.output_columns is None:
@@ -160,11 +165,7 @@ class IndicatorMetadata(BaseMetadata):
@dataclass
class ResearchMetadata(BaseMetadata):
"""Metadata for research scripts."""
conda_packages: list[str] = None # Additional conda packages required
def __post_init__(self):
if self.conda_packages is None:
self.conda_packages = []
pass
# Metadata class registry
@@ -503,6 +504,7 @@ class CategoryFileManager:
category: str,
name: str,
description: str,
details: str,
code: str,
metadata: Optional[dict] = None
) -> dict[str, Any]:
@@ -513,6 +515,7 @@ class CategoryFileManager:
category: Category name (strategy, indicator, research)
name: Display name for the item
description: LLM-generated description (required)
details: Full markdown description with enough detail to reproduce the code (required)
code: Python implementation code
metadata: Additional category-specific metadata fields
@@ -547,6 +550,13 @@ class CategoryFileManager:
meta_dict = metadata or {}
meta_dict["name"] = name
meta_dict["description"] = description
meta_dict["details"] = details
# For indicators, store the canonical pandas_ta_name so the reverse
# mapping (ta_name → directory) is reliable regardless of name casing.
if cat == Category.INDICATOR:
sanitized = sanitize_name(name).lower()
meta_dict["pandas_ta_name"] = f"custom_{sanitized}"
# Validate and write metadata
try:
@@ -592,6 +602,8 @@ class CategoryFileManager:
code: Optional[str] = None,
patches: Optional[list[dict]] = None,
description: Optional[str] = None,
details: Optional[str] = None,
detail_patches: Optional[list[dict]] = None,
metadata: Optional[dict] = None
) -> dict[str, Any]:
"""
@@ -603,6 +615,8 @@ class CategoryFileManager:
code: Full Python implementation code to replace existing (optional)
patches: List of {old_string, new_string} replacements (optional, preferred for small changes)
description: Updated description (optional, omit to keep existing)
details: Full replacement for the details field (optional, mutually exclusive with detail_patches)
detail_patches: List of {old_string, new_string} replacements applied to the details field (optional)
metadata: Additional metadata updates (optional)
Returns:
@@ -614,6 +628,8 @@ class CategoryFileManager:
"""
if code is not None and patches is not None:
return {"success": False, "error": "Provide either 'code' or 'patches', not both"}
if details is not None and detail_patches is not None:
return {"success": False, "error": "Provide either 'details' or 'detail_patches', not both"}
try:
cat = Category(category)
@@ -664,10 +680,25 @@ class CategoryFileManager:
except Exception as e:
return {"success": False, "error": f"Failed to write implementation: {e}"}
# Apply text-replacement patches to details field if provided
if detail_patches is not None:
current_details = existing_meta.get("details", "")
for i, patch in enumerate(detail_patches):
old = patch.get("old_string", "")
new = patch.get("new_string", "")
if old not in current_details:
return {"success": False, "error": f"Detail patch {i}: old_string not found in details"}
if current_details.count(old) > 1:
return {"success": False, "error": f"Detail patch {i}: old_string is not unique — add more surrounding context"}
current_details = current_details.replace(old, new, 1)
details = current_details
# Update metadata
updated_meta = existing_meta.copy()
if description is not None:
updated_meta["description"] = description
if details is not None:
updated_meta["details"] = details
if metadata:
updated_meta.update(metadata)

View File

@@ -197,26 +197,6 @@ def _get_env_yml() -> Optional[Path]:
return p if p.exists() else None
def _populate_indicator_types_from_disk(workspace_store, category_manager) -> None:
"""Scan existing indicators and add any missing entries to indicator_types store."""
existing = workspace_store.read('indicator_types')
existing_types = (existing.get('data') or {}).get('types') or {}
list_result = category_manager.list_items('indicator')
items = list_result.get('items', [])
added = 0
for item in items:
item_name = item.get('name', '')
if not item_name:
continue
pandas_ta_name = f"custom_{sanitize_name(item_name).lower()}"
if pandas_ta_name not in existing_types:
_upsert_indicator_type(workspace_store, category_manager, item_name)
added += 1
if added > 0:
logging.info(f"Populated {added} indicator type(s) from disk into indicator_types store")
# =============================================================================
# Configuration
@@ -436,6 +416,15 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"type": "string",
"description": "LLM-generated description of what this does (required)"
},
"details": {
"type": "string",
"description": (
"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)."
)
},
"code": {
"type": "string",
"description": "Python implementation code"
@@ -453,7 +442,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
)
}
},
"required": ["category", "name", "description", "code"]
"required": ["category", "name", "description", "details", "code"]
}
),
Tool(
@@ -500,6 +489,27 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
"type": "string",
"description": "Updated description (optional, omit to keep existing)"
},
"details": {
"type": "string",
"description": "Full replacement for the details field. Use only when rewriting the entire description; prefer 'detail_patches' for targeted edits."
},
"detail_patches": {
"type": "array",
"description": (
"Targeted edits to the details field as old/new string pairs. Preferred over 'details' for small changes. "
"Each patch: {\"old_string\": \"exact text to find\", \"new_string\": \"replacement text\"}. "
"old_string must be unique in the details field (add surrounding context if needed). "
"Patches are applied in order. Mutually exclusive with 'details'."
),
"items": {
"type": "object",
"properties": {
"old_string": {"type": "string"},
"new_string": {"type": "string"}
},
"required": ["old_string", "new_string"]
}
},
"metadata": {
"type": "object",
"description": (
@@ -912,6 +922,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
category=arguments.get("category", ""),
name=arguments.get("name", ""),
description=arguments.get("description", ""),
details=arguments.get("details", ""),
code=arguments.get("code", ""),
metadata=arguments.get("metadata")
)
@@ -947,6 +958,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
code=arguments.get("code"),
patches=arguments.get("patches"),
description=arguments.get("description"),
details=arguments.get("details"),
detail_patches=arguments.get("detail_patches"),
metadata=arguments.get("metadata")
)
content = []