diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9ee89..853e6e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.2] - 2025-11-07 + +### Fixed +- **Critical:** Fixed negative cash position bug where trades calculated from initial capital instead of accumulating + - Root cause: MCP tools return `CallToolResult` objects with position data in `structuredContent` field, but `ContextInjector` was checking `isinstance(result, dict)` which always failed + - Impact: Each trade checked cash against initial $10,000 instead of cumulative position, allowing over-spending and resulting in negative cash balances (e.g., -$8,768.68 after 11 trades totaling $18,768.68) + - Solution: Updated `ContextInjector` to extract position dict from `CallToolResult.structuredContent` before validation + - Fix ensures proper intra-day position tracking with cumulative cash checks preventing over-trading + - Updated unit tests to mock `CallToolResult` objects matching production MCP behavior + - Locations: `agent/context_injector.py:95-109`, `tests/unit/test_context_injector.py:26-53` +- Enabled MCP service logging by redirecting stdout/stderr from `/dev/null` to main process for better debugging + - Previously, all MCP tool debug output was silently discarded + - Now visible in docker logs for diagnosing parameter injection and trade execution issues + - Location: `agent_tools/start_mcp_services.py:81-88` + ### Fixed - **Critical:** Fixed stale jobs blocking new jobs after Docker container restart - Root cause: Jobs with status 'pending', 'downloading_data', or 'running' remained in database after container shutdown, preventing new job creation diff --git a/agent/context_injector.py b/agent/context_injector.py index 42a07bb..0cae567 100644 --- a/agent/context_injector.py +++ b/agent/context_injector.py @@ -88,28 +88,17 @@ class ContextInjector: # Update position state after successful trade if request.name in ["buy", "sell"]: - # Debug: Log result type and structure - print(f"[DEBUG ContextInjector] Trade result type: {type(result)}") - print(f"[DEBUG ContextInjector] Trade result: {result}") - # Extract position dict from MCP result # MCP tools return CallToolResult objects with structuredContent field position_dict = None if hasattr(result, 'structuredContent') and result.structuredContent: position_dict = result.structuredContent - print(f"[DEBUG ContextInjector] Extracted from structuredContent: {position_dict}") elif isinstance(result, dict): position_dict = result - print(f"[DEBUG ContextInjector] Using result as dict: {position_dict}") # Check if position dict is valid (not an error) and update state if position_dict and "error" not in position_dict and "CASH" in position_dict: # Update our tracked position with the new state self._current_position = position_dict.copy() - print(f"[DEBUG ContextInjector] Updated _current_position: {self._current_position}") - else: - print(f"[DEBUG ContextInjector] Did NOT update _current_position - check failed") - print(f"[DEBUG ContextInjector] position_dict: {position_dict}") - print(f"[DEBUG ContextInjector] _current_position remains: {self._current_position}") return result diff --git a/agent_tools/tool_trade.py b/agent_tools/tool_trade.py index 4b8c411..cc79f2f 100644 --- a/agent_tools/tool_trade.py +++ b/agent_tools/tool_trade.py @@ -132,14 +132,11 @@ def _buy_impl(symbol: str, amount: int, signature: str = None, today_date: str = # Step 1: Get current position # Use injected position if available (for intra-day tracking), # otherwise query database for starting position - print(f"[DEBUG buy] _current_position received: {_current_position}") if _current_position is not None: current_position = _current_position next_action_id = 0 # Not used in new schema - print(f"[DEBUG buy] Using injected position: {current_position}") else: current_position, next_action_id = get_current_position_from_db(job_id, signature, today_date) - print(f"[DEBUG buy] Queried position from DB: {current_position}") # Step 2: Get stock price try: @@ -192,8 +189,6 @@ def _buy_impl(symbol: str, amount: int, signature: str = None, today_date: str = conn.commit() print(f"[buy] {signature} bought {amount} shares of {symbol} at ${this_symbol_price}") - print(f"[DEBUG buy] Returning new_position: {new_position}") - print(f"[DEBUG buy] new_position keys: {list(new_position.keys())}") return new_position except Exception as e: