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)