mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-09 12:17:24 -04:00
Compare commits
2 Commits
v0.4.2-alp
...
v0.4.3-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 4638c073e3 | |||
| 96f61cf347 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### Fixed
|
||||||
- **Critical:** Fixed stale jobs blocking new jobs after Docker container restart
|
- **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
|
- Root cause: Jobs with status 'pending', 'downloading_data', or 'running' remained in database after container shutdown, preventing new job creation
|
||||||
|
|||||||
@@ -88,28 +88,17 @@ class ContextInjector:
|
|||||||
|
|
||||||
# Update position state after successful trade
|
# Update position state after successful trade
|
||||||
if request.name in ["buy", "sell"]:
|
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
|
# Extract position dict from MCP result
|
||||||
# MCP tools return CallToolResult objects with structuredContent field
|
# MCP tools return CallToolResult objects with structuredContent field
|
||||||
position_dict = None
|
position_dict = None
|
||||||
if hasattr(result, 'structuredContent') and result.structuredContent:
|
if hasattr(result, 'structuredContent') and result.structuredContent:
|
||||||
position_dict = result.structuredContent
|
position_dict = result.structuredContent
|
||||||
print(f"[DEBUG ContextInjector] Extracted from structuredContent: {position_dict}")
|
|
||||||
elif isinstance(result, dict):
|
elif isinstance(result, dict):
|
||||||
position_dict = result
|
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
|
# 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:
|
if position_dict and "error" not in position_dict and "CASH" in position_dict:
|
||||||
# Update our tracked position with the new state
|
# Update our tracked position with the new state
|
||||||
self._current_position = position_dict.copy()
|
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
|
return result
|
||||||
|
|||||||
@@ -132,14 +132,11 @@ def _buy_impl(symbol: str, amount: int, signature: str = None, today_date: str =
|
|||||||
# Step 1: Get current position
|
# Step 1: Get current position
|
||||||
# Use injected position if available (for intra-day tracking),
|
# Use injected position if available (for intra-day tracking),
|
||||||
# otherwise query database for starting position
|
# otherwise query database for starting position
|
||||||
print(f"[DEBUG buy] _current_position received: {_current_position}")
|
|
||||||
if _current_position is not None:
|
if _current_position is not None:
|
||||||
current_position = _current_position
|
current_position = _current_position
|
||||||
next_action_id = 0 # Not used in new schema
|
next_action_id = 0 # Not used in new schema
|
||||||
print(f"[DEBUG buy] Using injected position: {current_position}")
|
|
||||||
else:
|
else:
|
||||||
current_position, next_action_id = get_current_position_from_db(job_id, signature, today_date)
|
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
|
# Step 2: Get stock price
|
||||||
try:
|
try:
|
||||||
@@ -192,8 +189,6 @@ def _buy_impl(symbol: str, amount: int, signature: str = None, today_date: str =
|
|||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
print(f"[buy] {signature} bought {amount} shares of {symbol} at ${this_symbol_price}")
|
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
|
return new_position
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -611,6 +611,10 @@ class Database:
|
|||||||
|
|
||||||
Handles weekends/holidays by finding actual previous trading day.
|
Handles weekends/holidays by finding actual previous trading day.
|
||||||
|
|
||||||
|
NOTE: Queries across ALL jobs for the given model to enable portfolio
|
||||||
|
continuity even when new jobs are created with overlapping date ranges.
|
||||||
|
The job_id parameter is kept for API compatibility but not used in the query.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict with keys: id, date, ending_cash, ending_portfolio_value
|
dict with keys: id, date, ending_cash, ending_portfolio_value
|
||||||
or None if no previous day exists
|
or None if no previous day exists
|
||||||
@@ -619,11 +623,11 @@ class Database:
|
|||||||
"""
|
"""
|
||||||
SELECT id, date, ending_cash, ending_portfolio_value
|
SELECT id, date, ending_cash, ending_portfolio_value
|
||||||
FROM trading_days
|
FROM trading_days
|
||||||
WHERE job_id = ? AND model = ? AND date < ?
|
WHERE model = ? AND date < ?
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
""",
|
""",
|
||||||
(job_id, model, current_date)
|
(model, current_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
|
|||||||
@@ -130,6 +130,44 @@ class TestDatabaseHelpers:
|
|||||||
assert previous is not None
|
assert previous is not None
|
||||||
assert previous["date"] == "2025-01-17"
|
assert previous["date"] == "2025-01-17"
|
||||||
|
|
||||||
|
def test_get_previous_trading_day_across_jobs(self, db):
|
||||||
|
"""Test retrieving previous trading day from different job (cross-job continuity)."""
|
||||||
|
# Setup: Create two jobs
|
||||||
|
db.connection.execute(
|
||||||
|
"INSERT INTO jobs (job_id, status, config_path, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
("job-1", "completed", "config.json", "2025-10-07,2025-10-07", "deepseek-chat-v3.1", "2025-11-07T00:00:00Z")
|
||||||
|
)
|
||||||
|
db.connection.execute(
|
||||||
|
"INSERT INTO jobs (job_id, status, config_path, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
("job-2", "running", "config.json", "2025-10-08,2025-10-08", "deepseek-chat-v3.1", "2025-11-07T01:00:00Z")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Day 1 in job-1
|
||||||
|
db.create_trading_day(
|
||||||
|
job_id="job-1",
|
||||||
|
model="deepseek-chat-v3.1",
|
||||||
|
date="2025-10-07",
|
||||||
|
starting_cash=10000.0,
|
||||||
|
starting_portfolio_value=10000.0,
|
||||||
|
daily_profit=214.58,
|
||||||
|
daily_return_pct=2.15,
|
||||||
|
ending_cash=123.59,
|
||||||
|
ending_portfolio_value=10214.58
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test: Get previous day from job-2 on next date
|
||||||
|
# Should find job-1's record (cross-job continuity)
|
||||||
|
previous = db.get_previous_trading_day(
|
||||||
|
job_id="job-2",
|
||||||
|
model="deepseek-chat-v3.1",
|
||||||
|
current_date="2025-10-08"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert previous is not None
|
||||||
|
assert previous["date"] == "2025-10-07"
|
||||||
|
assert previous["ending_cash"] == 123.59
|
||||||
|
assert previous["ending_portfolio_value"] == 10214.58
|
||||||
|
|
||||||
def test_get_ending_holdings(self, db):
|
def test_get_ending_holdings(self, db):
|
||||||
"""Test retrieving ending holdings for a trading day."""
|
"""Test retrieving ending holdings for a trading day."""
|
||||||
db.connection.execute(
|
db.connection.execute(
|
||||||
|
|||||||
Reference in New Issue
Block a user