mirror of
https://github.com/Xe138/AI-Trader.git
synced 2026-04-01 17:17:24 -04:00
- Implementation plan with 9 tasks covering bug fixes and testing - Summary report documenting root causes, solution, and verification - Both documents provide comprehensive reference for future maintainers
1291 lines
43 KiB
Markdown
1291 lines
43 KiB
Markdown
# Fix Position Tracking and P&L Calculation Bugs Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Fix three critical bugs in position tracking: (1) cash reset to initial value each day, (2) positions lost over weekends, (3) incorrect profit calculations showing trades as losses.
|
|
|
|
**Architecture:** Remove redundant `_write_results_to_db()` method that creates corrupt position records with cash=0 and holdings=[], and fix profit calculation logic to compare same-day position values instead of previous-day portfolio value.
|
|
|
|
**Tech Stack:** Python 3.12, SQLite3, pytest
|
|
|
|
**Root Cause Analysis:**
|
|
|
|
1. **Bug #1 & #2 (Cash reset + positions lost):**
|
|
- `ModelDayExecutor._write_results_to_db()` calls non-existent methods (`get_positions()`, `get_last_trade()`, `get_current_prices()`)
|
|
- These return empty values, creating corrupt records with `cash=0`, `holdings=[]`
|
|
- `get_current_position_from_db()` then finds this corrupt record as "latest", causing reset
|
|
|
|
2. **Bug #3 (Incorrect profit calculations):**
|
|
- Current logic compares portfolio value to **previous day's final value**
|
|
- When you buy stocks, cash decreases and stock value increases equally → portfolio value unchanged → profit=0 shown
|
|
- Should compare to **start of current day** (after price changes) to show actual gains/losses from trading
|
|
|
|
---
|
|
|
|
## Task 1: Write Failing Tests for Current Bugs
|
|
|
|
**Files:**
|
|
- Create: `tests/unit/test_position_tracking_bugs.py`
|
|
|
|
**Step 1: Write test demonstrating cash reset bug**
|
|
|
|
Create `tests/unit/test_position_tracking_bugs.py`:
|
|
|
|
```python
|
|
"""
|
|
Tests demonstrating position tracking bugs before fix.
|
|
|
|
These tests should FAIL before implementing fixes, and PASS after.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from api.database import get_db_connection, initialize_database
|
|
from api.job_manager import JobManager
|
|
from agent_tools.tool_trade import _buy_impl
|
|
from tools.price_tools import add_no_trade_record_to_db
|
|
|
|
|
|
@pytest.fixture
|
|
def test_db_with_prices(tmp_path):
|
|
"""Create test database with price data."""
|
|
db_path = str(tmp_path / "test.db")
|
|
initialize_database(db_path)
|
|
|
|
# Insert price data for testing
|
|
conn = get_db_connection(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# 2025-10-06 prices
|
|
cursor.execute("""
|
|
INSERT INTO price_data (symbol, date, open, high, low, close, volume, created_at)
|
|
VALUES ('NVDA', '2025-10-06', 185.5, 190.0, 185.0, 188.0, 1000000, ?)
|
|
""", (datetime.utcnow().isoformat() + "Z",))
|
|
|
|
# 2025-10-07 prices (Monday after weekend)
|
|
cursor.execute("""
|
|
INSERT INTO price_data (symbol, date, open, high, low, close, volume, created_at)
|
|
VALUES ('NVDA', '2025-10-07', 186.23, 190.0, 186.0, 189.0, 1000000, ?)
|
|
""", (datetime.utcnow().isoformat() + "Z",))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return db_path
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPositionTrackingBugs:
|
|
"""Tests demonstrating the three critical bugs."""
|
|
|
|
def test_cash_not_reset_between_days(self, test_db_with_prices):
|
|
"""
|
|
Bug #1: Cash should carry over from previous day, not reset to initial value.
|
|
|
|
Scenario:
|
|
- Day 1: Start with $10,000, buy 5 NVDA @ $185.50 = $927.50, cash left = $9,072.50
|
|
- Day 2: Should start with $9,072.50 cash, not $10,000
|
|
"""
|
|
# Create job
|
|
manager = JobManager(db_path=test_db_with_prices)
|
|
job_id = manager.create_job(
|
|
config_path="configs/test.json",
|
|
date_range=["2025-10-06", "2025-10-07"],
|
|
models=["claude-sonnet-4.5"]
|
|
)
|
|
|
|
# Day 1: Initial position (action_id=0)
|
|
conn = get_db_connection(test_db_with_prices)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, started_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (job_id, "2025-10-06", "claude-sonnet-4.5", datetime.utcnow().isoformat() + "Z"))
|
|
session_id_day1 = cursor.lastrowid
|
|
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type,
|
|
cash, portfolio_value, session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
job_id, "2025-10-06", "claude-sonnet-4.5", 0, "no_trade",
|
|
10000.0, 10000.0, session_id_day1, datetime.utcnow().isoformat() + "Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Day 1: Buy 5 NVDA @ $185.50
|
|
result = _buy_impl(
|
|
symbol="NVDA",
|
|
amount=5,
|
|
signature="claude-sonnet-4.5",
|
|
today_date="2025-10-06",
|
|
job_id=job_id,
|
|
session_id=session_id_day1
|
|
)
|
|
|
|
assert "error" not in result
|
|
assert result["CASH"] == 9072.5 # 10000 - (5 * 185.5)
|
|
|
|
# Day 2: Create new session
|
|
conn = get_db_connection(test_db_with_prices)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, started_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (job_id, "2025-10-07", "claude-sonnet-4.5", datetime.utcnow().isoformat() + "Z"))
|
|
session_id_day2 = cursor.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Day 2: Check starting cash (should be $9,072.50, not $10,000)
|
|
from agent_tools.tool_trade import get_current_position_from_db
|
|
|
|
position, next_action_id = get_current_position_from_db(
|
|
job_id=job_id,
|
|
model="claude-sonnet-4.5",
|
|
date="2025-10-07"
|
|
)
|
|
|
|
# BUG: This will fail before fix - cash resets to $10,000 or $0
|
|
assert position["CASH"] == 9072.5, f"Expected cash $9,072.50 but got ${position['CASH']}"
|
|
assert position["NVDA"] == 5, f"Expected 5 NVDA shares but got {position.get('NVDA', 0)}"
|
|
|
|
def test_positions_persist_over_weekend(self, test_db_with_prices):
|
|
"""
|
|
Bug #2: Positions should persist over non-trading days (weekends).
|
|
|
|
Scenario:
|
|
- Friday 2025-10-06: Buy 5 NVDA
|
|
- Monday 2025-10-07: Should still have 5 NVDA
|
|
"""
|
|
# Create job
|
|
manager = JobManager(db_path=test_db_with_prices)
|
|
job_id = manager.create_job(
|
|
config_path="configs/test.json",
|
|
date_range=["2025-10-06", "2025-10-07"],
|
|
models=["claude-sonnet-4.5"]
|
|
)
|
|
|
|
# Friday: Initial position + buy
|
|
conn = get_db_connection(test_db_with_prices)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, started_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (job_id, "2025-10-06", "claude-sonnet-4.5", datetime.utcnow().isoformat() + "Z"))
|
|
session_id = cursor.lastrowid
|
|
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type,
|
|
cash, portfolio_value, session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
job_id, "2025-10-06", "claude-sonnet-4.5", 0, "no_trade",
|
|
10000.0, 10000.0, session_id, datetime.utcnow().isoformat() + "Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
_buy_impl(
|
|
symbol="NVDA",
|
|
amount=5,
|
|
signature="claude-sonnet-4.5",
|
|
today_date="2025-10-06",
|
|
job_id=job_id,
|
|
session_id=session_id
|
|
)
|
|
|
|
# Monday: Check positions persist
|
|
from agent_tools.tool_trade import get_current_position_from_db
|
|
|
|
position, _ = get_current_position_from_db(
|
|
job_id=job_id,
|
|
model="claude-sonnet-4.5",
|
|
date="2025-10-07"
|
|
)
|
|
|
|
# BUG: This will fail before fix - positions lost, holdings=[]
|
|
assert "NVDA" in position, "NVDA position should persist over weekend"
|
|
assert position["NVDA"] == 5, f"Expected 5 NVDA shares but got {position.get('NVDA', 0)}"
|
|
|
|
def test_profit_calculation_accuracy(self, test_db_with_prices):
|
|
"""
|
|
Bug #3: Profit should reflect actual gains/losses, not show trades as losses.
|
|
|
|
Scenario:
|
|
- Start with $10,000 cash, portfolio value = $10,000
|
|
- Buy 5 NVDA @ $185.50 = $927.50
|
|
- New position: cash = $9,072.50, 5 NVDA worth $927.50
|
|
- Portfolio value = $9,072.50 + $927.50 = $10,000 (unchanged)
|
|
- Expected profit = $0 (no price change yet, just traded)
|
|
|
|
Current bug: Shows profit = -$927.50 or similar (treating trade as loss)
|
|
"""
|
|
# Create job
|
|
manager = JobManager(db_path=test_db_with_prices)
|
|
job_id = manager.create_job(
|
|
config_path="configs/test.json",
|
|
date_range=["2025-10-06"],
|
|
models=["claude-sonnet-4.5"]
|
|
)
|
|
|
|
# Create session and initial position
|
|
conn = get_db_connection(test_db_with_prices)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
INSERT INTO trading_sessions (job_id, date, model, started_at)
|
|
VALUES (?, ?, ?, ?)
|
|
""", (job_id, "2025-10-06", "claude-sonnet-4.5", datetime.utcnow().isoformat() + "Z"))
|
|
session_id = cursor.lastrowid
|
|
|
|
cursor.execute("""
|
|
INSERT INTO positions (
|
|
job_id, date, model, action_id, action_type,
|
|
cash, portfolio_value, daily_profit, daily_return_pct,
|
|
session_id, created_at
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
job_id, "2025-10-06", "claude-sonnet-4.5", 0, "no_trade",
|
|
10000.0, 10000.0, None, None,
|
|
session_id, datetime.utcnow().isoformat() + "Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
# Buy 5 NVDA @ $185.50
|
|
_buy_impl(
|
|
symbol="NVDA",
|
|
amount=5,
|
|
signature="claude-sonnet-4.5",
|
|
today_date="2025-10-06",
|
|
job_id=job_id,
|
|
session_id=session_id
|
|
)
|
|
|
|
# Check profit calculation
|
|
conn = get_db_connection(test_db_with_prices)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT portfolio_value, daily_profit, daily_return_pct
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date = ? AND action_id = 1
|
|
""", (job_id, "claude-sonnet-4.5", "2025-10-06"))
|
|
|
|
row = cursor.fetchone()
|
|
conn.close()
|
|
|
|
portfolio_value = row[0]
|
|
daily_profit = row[1]
|
|
daily_return_pct = row[2]
|
|
|
|
# Portfolio value should be $10,000 (cash $9,072.50 + 5 NVDA @ $185.50)
|
|
assert abs(portfolio_value - 10000.0) < 0.01, \
|
|
f"Expected portfolio value $10,000 but got ${portfolio_value}"
|
|
|
|
# BUG: This will fail before fix - shows profit as negative or zero when should be zero
|
|
# Profit should be $0 (no price movement, just traded)
|
|
assert abs(daily_profit) < 0.01, \
|
|
f"Expected profit $0 (no price change) but got ${daily_profit}"
|
|
assert abs(daily_return_pct) < 0.01, \
|
|
f"Expected return 0% but got {daily_return_pct}%"
|
|
```
|
|
|
|
**Step 2: Run tests to verify they fail**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/unit/test_position_tracking_bugs.py -v`
|
|
|
|
Expected: All 3 tests FAIL demonstrating the bugs
|
|
|
|
**Step 3: Commit failing tests**
|
|
|
|
```bash
|
|
git add tests/unit/test_position_tracking_bugs.py
|
|
git commit -m "test: add failing tests demonstrating position tracking bugs"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Remove Redundant `_write_results_to_db()` Method
|
|
|
|
**Files:**
|
|
- Modify: `api/model_day_executor.py:161-167` (remove call)
|
|
- Modify: `api/model_day_executor.py:435-558` (remove entire method)
|
|
|
|
**Step 1: Remove the call to `_write_results_to_db()`**
|
|
|
|
In `api/model_day_executor.py`, find the `execute_async()` method around line 161-167:
|
|
|
|
```python
|
|
# Commit and close connection before _write_results_to_db opens a new one
|
|
conn.commit()
|
|
conn.close()
|
|
conn = None # Mark as closed
|
|
|
|
# Store positions (pass session_id) - this opens its own connection
|
|
self._write_results_to_db(agent, session_id)
|
|
|
|
# Update status to completed
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```python
|
|
# Commit and close connection
|
|
conn.commit()
|
|
conn.close()
|
|
conn = None # Mark as closed
|
|
|
|
# Note: Positions are written by trade tools (buy/sell) or no_trade_record
|
|
# No need to write positions here - that was creating duplicate/corrupt records
|
|
|
|
# Update status to completed
|
|
```
|
|
|
|
**Step 2: Delete the entire `_write_results_to_db()` method**
|
|
|
|
In `api/model_day_executor.py`, delete lines 435-558 (the entire method):
|
|
|
|
```python
|
|
# DELETE THIS ENTIRE METHOD (lines 435-558):
|
|
def _write_results_to_db(self, agent, session_id: int) -> None:
|
|
"""
|
|
Write execution results to SQLite.
|
|
...
|
|
"""
|
|
# ... entire method body ...
|
|
```
|
|
|
|
Also delete the helper method `_calculate_portfolio_value()` at lines 533-558:
|
|
|
|
```python
|
|
# DELETE THIS TOO (lines 533-558):
|
|
def _calculate_portfolio_value(
|
|
self,
|
|
positions: Dict[str, float],
|
|
current_prices: Dict[str, float]
|
|
) -> float:
|
|
"""
|
|
Calculate total portfolio value.
|
|
...
|
|
"""
|
|
# ... entire method body ...
|
|
```
|
|
|
|
**Step 3: Run unit tests to see what breaks**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/unit/test_model_day_executor.py -v`
|
|
|
|
Expected: Some tests FAIL because they mock non-existent methods
|
|
|
|
**Step 4: Commit the removal**
|
|
|
|
```bash
|
|
git add api/model_day_executor.py
|
|
git commit -m "fix: remove redundant _write_results_to_db() creating corrupt position records"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Fix Unit Tests That Mock Non-Existent Methods
|
|
|
|
**Files:**
|
|
- Modify: `tests/unit/test_model_day_executor.py:21-43`
|
|
- Modify: `tests/unit/test_model_day_executor.py:185-295`
|
|
- Modify: `tests/unit/test_model_day_executor_reasoning.py:240-266`
|
|
|
|
**Step 1: Update `create_mock_agent()` helper**
|
|
|
|
In `tests/unit/test_model_day_executor.py`, find the `create_mock_agent()` function (lines 21-43):
|
|
|
|
```python
|
|
def create_mock_agent(positions=None, last_trade=None, current_prices=None,
|
|
reasoning_steps=None, tool_usage=None, session_result=None,
|
|
conversation_history=None):
|
|
"""Helper to create properly mocked agent."""
|
|
mock_agent = Mock()
|
|
|
|
# Default values
|
|
mock_agent.get_positions.return_value = positions or {"CASH": 10000.0}
|
|
mock_agent.get_last_trade.return_value = last_trade
|
|
mock_agent.get_current_prices.return_value = current_prices or {}
|
|
# ...
|
|
```
|
|
|
|
Replace with (remove references to deleted methods):
|
|
|
|
```python
|
|
def create_mock_agent(reasoning_steps=None, tool_usage=None, session_result=None,
|
|
conversation_history=None):
|
|
"""Helper to create properly mocked agent."""
|
|
mock_agent = Mock()
|
|
|
|
# Note: Removed get_positions, get_last_trade, get_current_prices
|
|
# These methods don't exist in BaseAgent and were only used by
|
|
# the now-deleted _write_results_to_db() method
|
|
|
|
mock_agent.get_reasoning_steps.return_value = reasoning_steps or []
|
|
mock_agent.get_tool_usage.return_value = tool_usage or {}
|
|
mock_agent.get_conversation_history.return_value = conversation_history or []
|
|
|
|
# Async methods - use AsyncMock
|
|
mock_agent.run_trading_session = AsyncMock(return_value=session_result or {"success": True})
|
|
mock_agent.generate_summary = AsyncMock(return_value="Mock summary")
|
|
mock_agent.summarize_message = AsyncMock(return_value="Mock message summary")
|
|
|
|
# Mock model for summary generation
|
|
mock_agent.model = Mock()
|
|
|
|
return mock_agent
|
|
```
|
|
|
|
**Step 2: Update tests that verify position writes**
|
|
|
|
In `tests/unit/test_model_day_executor.py`, find `TestModelDayExecutorDataPersistence` class (lines 182-345).
|
|
|
|
The tests `test_writes_position_to_database` and `test_writes_holdings_to_database` need to be updated because positions are now written by trade tools, not by the executor.
|
|
|
|
Replace these tests with:
|
|
|
|
```python
|
|
@pytest.mark.unit
|
|
class TestModelDayExecutorDataPersistence:
|
|
"""Test result persistence to SQLite."""
|
|
|
|
def test_creates_initial_position(self, clean_db):
|
|
"""Should create initial position record (action_id=0) on first day."""
|
|
from api.model_day_executor import ModelDayExecutor
|
|
from api.job_manager import JobManager
|
|
from api.database import get_db_connection
|
|
|
|
# Create job
|
|
manager = JobManager(db_path=clean_db)
|
|
job_id = manager.create_job(
|
|
config_path="configs/test.json",
|
|
date_range=["2025-01-16"],
|
|
models=["gpt-5"]
|
|
)
|
|
|
|
# Mock successful execution (no trades)
|
|
mock_agent = create_mock_agent(
|
|
session_result={"success": True, "total_steps": 10}
|
|
)
|
|
|
|
with patch("api.model_day_executor.RuntimeConfigManager") as mock_runtime:
|
|
mock_instance = Mock()
|
|
mock_instance.create_runtime_config.return_value = "/tmp/runtime_test.json"
|
|
mock_runtime.return_value = mock_instance
|
|
|
|
executor = ModelDayExecutor(
|
|
job_id=job_id,
|
|
date="2025-01-16",
|
|
model_sig="gpt-5",
|
|
config_path="configs/test.json",
|
|
db_path=clean_db
|
|
)
|
|
|
|
with patch.object(executor, '_initialize_agent', return_value=mock_agent):
|
|
# Mock _handle_trading_result to avoid database writes
|
|
with patch.object(executor, '_handle_trading_result', new_callable=AsyncMock):
|
|
executor.execute()
|
|
|
|
# Verify initial position created (action_id=0)
|
|
conn = get_db_connection(clean_db)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
SELECT job_id, date, model, action_id, action_type, cash, portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND date = ? AND model = ?
|
|
""", (job_id, "2025-01-16", "gpt-5"))
|
|
|
|
row = cursor.fetchone()
|
|
assert row is not None, "Should create initial position record"
|
|
assert row[0] == job_id
|
|
assert row[1] == "2025-01-16"
|
|
assert row[2] == "gpt-5"
|
|
assert row[3] == 0, "Initial position should have action_id=0"
|
|
assert row[4] == "no_trade"
|
|
assert row[5] == 10000.0, "Initial cash should be $10,000"
|
|
assert row[6] == 10000.0, "Initial portfolio value should be $10,000"
|
|
|
|
conn.close()
|
|
|
|
def test_writes_reasoning_logs(self, clean_db):
|
|
"""Should write AI reasoning logs to SQLite."""
|
|
# This test remains the same as before (line 297-344)
|
|
# ... (keep existing test)
|
|
```
|
|
|
|
**Step 3: Update `test_model_day_executor_reasoning.py`**
|
|
|
|
In `tests/unit/test_model_day_executor_reasoning.py`, find the test that calls `_write_results_to_db` directly (around line 240-266).
|
|
|
|
Delete or skip this test since the method no longer exists:
|
|
|
|
```python
|
|
@pytest.mark.skip(reason="Method _write_results_to_db() removed - positions written by trade tools")
|
|
def test_write_results_links_position_to_session(test_db):
|
|
"""DEPRECATED: This test verified _write_results_to_db() which has been removed."""
|
|
# Delete this entire test or mark as skipped
|
|
pass
|
|
```
|
|
|
|
**Step 4: Run tests to verify fixes**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/unit/test_model_day_executor.py tests/unit/test_model_day_executor_reasoning.py -v`
|
|
|
|
Expected: All tests PASS
|
|
|
|
**Step 5: Commit test fixes**
|
|
|
|
```bash
|
|
git add tests/unit/test_model_day_executor.py tests/unit/test_model_day_executor_reasoning.py
|
|
git commit -m "test: update tests after removing _write_results_to_db()"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Fix Profit Calculation Logic (Bug #3)
|
|
|
|
**Files:**
|
|
- Modify: `agent_tools/tool_trade.py:144-157` (buy function)
|
|
- Modify: `agent_tools/tool_trade.py:287-300` (sell function)
|
|
- Modify: `tools/price_tools.py:417-430` (no_trade function)
|
|
|
|
**Background:**
|
|
|
|
Current profit calculation compares portfolio value to **previous day's final value**. This is incorrect because:
|
|
- When you buy stocks, cash ↓ and stock value ↑ equally → portfolio unchanged → profit=0
|
|
- But we're comparing to previous day's final value, which makes trades look like losses
|
|
|
|
**Correct approach:**
|
|
|
|
Profit should be calculated by comparing to the **start-of-day portfolio value** (same day, action_id=0). This shows actual gains/losses from price movements and trading decisions.
|
|
|
|
**Step 1: Fix profit calculation in buy function**
|
|
|
|
In `agent_tools/tool_trade.py`, find the profit calculation in `_buy_impl()` (around lines 144-157):
|
|
|
|
```python
|
|
# Get previous portfolio value for P&L calculation
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date < ?
|
|
ORDER BY date DESC, action_id DESC
|
|
LIMIT 1
|
|
""", (job_id, signature, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
previous_value = row[0] if row else 10000.0 # Default initial value
|
|
|
|
daily_profit = portfolio_value - previous_value
|
|
daily_return_pct = (daily_profit / previous_value * 100) if previous_value > 0 else 0
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```python
|
|
# Get start-of-day portfolio value (action_id=0 for today) for P&L calculation
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date = ? AND action_id = 0
|
|
LIMIT 1
|
|
""", (job_id, signature, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
# Compare to start of day (action_id=0)
|
|
start_of_day_value = row[0]
|
|
daily_profit = portfolio_value - start_of_day_value
|
|
daily_return_pct = (daily_profit / start_of_day_value * 100) if start_of_day_value > 0 else 0
|
|
else:
|
|
# First action of first day - no baseline yet
|
|
daily_profit = 0.0
|
|
daily_return_pct = 0.0
|
|
```
|
|
|
|
**Step 2: Fix profit calculation in sell function**
|
|
|
|
In `agent_tools/tool_trade.py`, find the profit calculation in `_sell_impl()` (around lines 287-300):
|
|
|
|
```python
|
|
# Get previous portfolio value
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date < ?
|
|
ORDER BY date DESC, action_id DESC
|
|
LIMIT 1
|
|
""", (job_id, signature, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
previous_value = row[0] if row else 10000.0
|
|
|
|
daily_profit = portfolio_value - previous_value
|
|
daily_return_pct = (daily_profit / previous_value * 100) if previous_value > 0 else 0
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```python
|
|
# Get start-of-day portfolio value (action_id=0 for today) for P&L calculation
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date = ? AND action_id = 0
|
|
LIMIT 1
|
|
""", (job_id, signature, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
# Compare to start of day (action_id=0)
|
|
start_of_day_value = row[0]
|
|
daily_profit = portfolio_value - start_of_day_value
|
|
daily_return_pct = (daily_profit / start_of_day_value * 100) if start_of_day_value > 0 else 0
|
|
else:
|
|
# First action of first day - no baseline yet
|
|
daily_profit = 0.0
|
|
daily_return_pct = 0.0
|
|
```
|
|
|
|
**Step 3: Fix profit calculation in no_trade function**
|
|
|
|
In `tools/price_tools.py`, find the profit calculation in `add_no_trade_record_to_db()` (around lines 417-430):
|
|
|
|
```python
|
|
# Get previous value for P&L
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date < ?
|
|
ORDER BY date DESC, action_id DESC
|
|
LIMIT 1
|
|
""", (job_id, modelname, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
previous_value = row[0] if row else 10000.0
|
|
|
|
daily_profit = portfolio_value - previous_value
|
|
daily_return_pct = (daily_profit / previous_value * 100) if previous_value > 0 else 0
|
|
```
|
|
|
|
Replace with:
|
|
|
|
```python
|
|
# Get start-of-day portfolio value (action_id=0 for today) for P&L calculation
|
|
cursor.execute("""
|
|
SELECT portfolio_value
|
|
FROM positions
|
|
WHERE job_id = ? AND model = ? AND date = ? AND action_id = 0
|
|
LIMIT 1
|
|
""", (job_id, modelname, today_date))
|
|
|
|
row = cursor.fetchone()
|
|
|
|
if row:
|
|
# Compare to start of day (action_id=0)
|
|
start_of_day_value = row[0]
|
|
daily_profit = portfolio_value - start_of_day_value
|
|
daily_return_pct = (daily_profit / start_of_day_value * 100) if start_of_day_value > 0 else 0
|
|
else:
|
|
# First action of first day - no baseline yet
|
|
daily_profit = 0.0
|
|
daily_return_pct = 0.0
|
|
```
|
|
|
|
**Step 4: Run bug tests to verify fix**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/unit/test_position_tracking_bugs.py::TestPositionTrackingBugs::test_profit_calculation_accuracy -v`
|
|
|
|
Expected: Test PASSES
|
|
|
|
**Step 5: Commit profit calculation fixes**
|
|
|
|
```bash
|
|
git add agent_tools/tool_trade.py tools/price_tools.py
|
|
git commit -m "fix: correct profit calculation to compare against start-of-day value"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Verify All Bug Tests Pass
|
|
|
|
**Step 1: Run all bug tests**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/unit/test_position_tracking_bugs.py -v`
|
|
|
|
Expected: All 3 tests PASS
|
|
|
|
**Step 2: Run full test suite**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/ -v --cov=. --cov-report=term-missing --cov-report=html --tb=short`
|
|
|
|
Expected: All tests PASS, coverage maintained or improved
|
|
|
|
**Step 3: If any tests fail, debug and fix**
|
|
|
|
If tests fail:
|
|
1. Read the error message carefully
|
|
2. Check which assertion failed
|
|
3. Add debug prints to understand state
|
|
4. Fix the issue
|
|
5. Re-run tests
|
|
6. Commit the fix
|
|
|
|
**Step 4: Commit passing tests**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "test: verify all position tracking bugs are fixed"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Integration Test with Real Simulation
|
|
|
|
**Files:**
|
|
- Create: `tests/integration/test_position_tracking_e2e.py`
|
|
|
|
**Step 1: Write end-to-end integration test**
|
|
|
|
Create `tests/integration/test_position_tracking_e2e.py`:
|
|
|
|
```python
|
|
"""
|
|
End-to-end integration test for position tracking across multiple days.
|
|
|
|
Tests the complete flow: ModelDayExecutor → trade tools → database → position retrieval
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
from api.database import get_db_connection, initialize_database
|
|
from api.job_manager import JobManager
|
|
from api.model_day_executor import ModelDayExecutor
|
|
from unittest.mock import patch, Mock, AsyncMock
|
|
|
|
|
|
def create_test_db_with_multi_day_prices(tmp_path):
|
|
"""Create test database with prices for multiple consecutive days."""
|
|
db_path = str(tmp_path / "test.db")
|
|
initialize_database(db_path)
|
|
|
|
conn = get_db_connection(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Create price data for 5 consecutive trading days
|
|
dates = ["2025-10-06", "2025-10-07", "2025-10-08", "2025-10-09", "2025-10-10"]
|
|
base_price = 185.0
|
|
|
|
for i, date in enumerate(dates):
|
|
# Prices gradually increase each day
|
|
open_price = base_price + (i * 2.0)
|
|
close_price = base_price + (i * 2.0) + 1.5
|
|
|
|
for symbol in ["NVDA", "AAPL", "MSFT"]:
|
|
cursor.execute("""
|
|
INSERT INTO price_data (symbol, date, open, high, low, close, volume, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""", (
|
|
symbol, date, open_price, open_price + 3, open_price - 1,
|
|
close_price, 1000000, datetime.utcnow().isoformat() + "Z"
|
|
))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
return db_path
|
|
|
|
|
|
@pytest.mark.integration
|
|
class TestPositionTrackingEndToEnd:
|
|
"""End-to-end tests for position tracking across multiple days."""
|
|
|
|
def test_multi_day_position_continuity(self, tmp_path):
|
|
"""
|
|
Test that positions correctly carry over across multiple trading days.
|
|
|
|
Scenario:
|
|
- Day 1: Start with $10,000, buy 5 NVDA @ $185
|
|
- Day 2: Cash should be $9,075, holdings should be 5 NVDA
|
|
- Day 3: Buy 3 AAPL @ $189
|
|
- Day 4: Should have 5 NVDA + 3 AAPL
|
|
- Day 5: Sell 2 NVDA @ $193
|
|
- Final: Should have 3 NVDA + 3 AAPL + increased cash
|
|
"""
|
|
db_path = create_test_db_with_multi_day_prices(tmp_path)
|
|
|
|
# Create job for 5-day simulation
|
|
manager = JobManager(db_path=db_path)
|
|
job_id = manager.create_job(
|
|
config_path="configs/test.json",
|
|
date_range=["2025-10-06", "2025-10-07", "2025-10-08", "2025-10-09", "2025-10-10"],
|
|
models=["test-model"]
|
|
)
|
|
|
|
# Mock agent that makes specific trades each day
|
|
def create_trading_agent(day, trades):
|
|
"""Create mock agent that executes specific trades."""
|
|
mock_agent = Mock()
|
|
mock_agent.get_conversation_history.return_value = []
|
|
|
|
# Mock async methods
|
|
async def run_session(date):
|
|
# Execute trades for this day
|
|
from agent_tools.tool_trade import _buy_impl, _sell_impl
|
|
from tools.general_tools import get_config_value
|
|
|
|
job_id = get_config_value("JOB_ID")
|
|
session_id = get_config_value("SESSION_ID")
|
|
|
|
for trade in trades:
|
|
if trade["action"] == "buy":
|
|
_buy_impl(
|
|
symbol=trade["symbol"],
|
|
amount=trade["amount"],
|
|
signature="test-model",
|
|
today_date=date,
|
|
job_id=job_id,
|
|
session_id=session_id
|
|
)
|
|
elif trade["action"] == "sell":
|
|
_sell_impl(
|
|
symbol=trade["symbol"],
|
|
amount=trade["amount"],
|
|
signature="test-model",
|
|
today_date=date,
|
|
job_id=job_id,
|
|
session_id=session_id
|
|
)
|
|
|
|
return {"success": True}
|
|
|
|
mock_agent.run_trading_session = AsyncMock(side_effect=run_session)
|
|
mock_agent.generate_summary = AsyncMock(return_value="Mock summary")
|
|
return mock_agent
|
|
|
|
# Day 1: Buy 5 NVDA
|
|
day1_trades = [{"action": "buy", "symbol": "NVDA", "amount": 5}]
|
|
# Day 2: No trades
|
|
day2_trades = []
|
|
# Day 3: Buy 3 AAPL
|
|
day3_trades = [{"action": "buy", "symbol": "AAPL", "amount": 3}]
|
|
# Day 4: No trades
|
|
day4_trades = []
|
|
# Day 5: Sell 2 NVDA
|
|
day5_trades = [{"action": "sell", "symbol": "NVDA", "amount": 2}]
|
|
|
|
all_trades = [day1_trades, day2_trades, day3_trades, day4_trades, day5_trades]
|
|
dates = ["2025-10-06", "2025-10-07", "2025-10-08", "2025-10-09", "2025-10-10"]
|
|
|
|
# Execute each day
|
|
for i, (date, trades) in enumerate(zip(dates, all_trades)):
|
|
with patch("api.model_day_executor.RuntimeConfigManager") as mock_runtime:
|
|
mock_instance = Mock()
|
|
mock_instance.create_runtime_config.return_value = f"/tmp/runtime_{i}.json"
|
|
mock_runtime.return_value = mock_instance
|
|
|
|
executor = ModelDayExecutor(
|
|
job_id=job_id,
|
|
date=date,
|
|
model_sig="test-model",
|
|
config_path="configs/test.json",
|
|
db_path=db_path
|
|
)
|
|
|
|
mock_agent = create_trading_agent(i, trades)
|
|
|
|
with patch.object(executor, '_initialize_agent', return_value=mock_agent):
|
|
result = executor.execute()
|
|
assert result["success"], f"Day {i+1} execution failed"
|
|
|
|
# Verify final positions
|
|
conn = get_db_connection(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
# Get last position
|
|
cursor.execute("""
|
|
SELECT p.cash, p.portfolio_value
|
|
FROM positions p
|
|
WHERE p.job_id = ? AND p.model = ?
|
|
ORDER BY p.date DESC, p.action_id DESC
|
|
LIMIT 1
|
|
""", (job_id, "test-model"))
|
|
|
|
final_position = cursor.fetchone()
|
|
|
|
# Get final holdings
|
|
cursor.execute("""
|
|
SELECT h.symbol, h.quantity
|
|
FROM holdings h
|
|
JOIN positions p ON h.position_id = p.id
|
|
WHERE p.job_id = ? AND p.model = ?
|
|
ORDER BY p.date DESC, p.action_id DESC, h.symbol
|
|
LIMIT 10
|
|
""", (job_id, "test-model"))
|
|
|
|
holdings = {row[0]: row[1] for row in cursor.fetchall()}
|
|
|
|
conn.close()
|
|
|
|
# Verify final state
|
|
assert "NVDA" in holdings, "Should have NVDA position"
|
|
assert holdings["NVDA"] == 3, f"Expected 3 NVDA (bought 5, sold 2) but got {holdings['NVDA']}"
|
|
|
|
assert "AAPL" in holdings, "Should have AAPL position"
|
|
assert holdings["AAPL"] == 3, f"Expected 3 AAPL but got {holdings['AAPL']}"
|
|
|
|
# Cash should be less than initial $10,000 (we bought more than sold)
|
|
assert final_position[0] < 10000, f"Cash should be less than $10,000 but got ${final_position[0]}"
|
|
|
|
# Portfolio value should be roughly $10,000 (prices didn't change much)
|
|
# Allow some variation due to price movements
|
|
assert 9800 < final_position[1] < 10200, \
|
|
f"Portfolio value should be ~$10,000 but got ${final_position[1]}"
|
|
```
|
|
|
|
**Step 2: Run integration test**
|
|
|
|
Run: `./venv/bin/python -m pytest tests/integration/test_position_tracking_e2e.py -v`
|
|
|
|
Expected: Test PASSES
|
|
|
|
**Step 3: Commit integration test**
|
|
|
|
```bash
|
|
git add tests/integration/test_position_tracking_e2e.py
|
|
git commit -m "test: add e2e integration test for multi-day position tracking"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Update Documentation
|
|
|
|
**Files:**
|
|
- Modify: `CHANGELOG.md`
|
|
- Modify: `docs/developer/database-schema.md`
|
|
|
|
**Step 1: Update CHANGELOG.md**
|
|
|
|
Add entry at the top of `CHANGELOG.md`:
|
|
|
|
```markdown
|
|
## [Unreleased]
|
|
|
|
### Fixed
|
|
- **Critical:** Fixed position tracking bugs causing cash reset and positions lost over weekends
|
|
- Removed redundant `ModelDayExecutor._write_results_to_db()` that created corrupt records with cash=0 and holdings=[]
|
|
- Fixed profit calculation to compare against start-of-day portfolio value instead of previous day's final value
|
|
- Positions now correctly carry over between trading days and across weekends
|
|
- Profit/loss calculations now accurately reflect trading gains/losses without treating trades as losses
|
|
|
|
### Changed
|
|
- Position tracking now exclusively handled by trade tools (`buy()`, `sell()`) and `add_no_trade_record_to_db()`
|
|
- Daily profit calculation compares to start-of-day (action_id=0) portfolio value for accurate P&L tracking
|
|
```
|
|
|
|
**Step 2: Update database schema documentation**
|
|
|
|
In `docs/developer/database-schema.md`, find the section on the `positions` table and update the `daily_profit` and `daily_return_pct` field descriptions:
|
|
|
|
```markdown
|
|
### positions
|
|
|
|
| Column | Type | Description |
|
|
|--------|------|-------------|
|
|
| ... | ... | ... |
|
|
| daily_profit | REAL | **Daily profit/loss compared to start-of-day portfolio value (action_id=0).** Calculated as: `current_portfolio_value - start_of_day_portfolio_value`. This shows the actual gain/loss from price movements and trading decisions, not affected by merely buying/selling stocks. |
|
|
| daily_return_pct | REAL | **Daily return percentage compared to start-of-day portfolio value.** Calculated as: `(daily_profit / start_of_day_portfolio_value) * 100` |
|
|
| ... | ... | ... |
|
|
|
|
**Important Notes:**
|
|
|
|
- **Position tracking flow:** Positions are written by trade tools (`buy()`, `sell()` in `agent_tools/tool_trade.py`) and no-trade records (`add_no_trade_record_to_db()` in `tools/price_tools.py`). Each trade creates a new position record.
|
|
|
|
- **Action ID sequence:**
|
|
- `action_id=0`: Start-of-day position (created by `ModelDayExecutor._initialize_starting_position()` on first day only)
|
|
- `action_id=1+`: Each trade or no-trade action increments the action_id
|
|
|
|
- **Profit calculation:** Daily profit is calculated by comparing current portfolio value to the **start-of-day** portfolio value (action_id=0 for the current date). This ensures that:
|
|
- Buying stocks doesn't show as a loss (cash ↓, stock value ↑ equally)
|
|
- Selling stocks doesn't show as a gain (cash ↑, stock value ↓ equally)
|
|
- Only actual price movements and strategic trading show as profit/loss
|
|
```
|
|
|
|
**Step 3: Commit documentation updates**
|
|
|
|
```bash
|
|
git add CHANGELOG.md docs/developer/database-schema.md
|
|
git commit -m "docs: update changelog and schema docs for position tracking fixes"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Manual Testing with Real Simulation
|
|
|
|
**Step 1: Create test configuration**
|
|
|
|
Create `configs/position_tracking_test.json`:
|
|
|
|
```json
|
|
{
|
|
"agent_type": "BaseAgent",
|
|
"date_range": {
|
|
"init_date": "2025-10-06",
|
|
"end_date": "2025-10-10"
|
|
},
|
|
"models": [
|
|
{
|
|
"name": "position-test",
|
|
"basemodel": "anthropic/claude-sonnet-4-20250514",
|
|
"signature": "position-tracking-test",
|
|
"enabled": true
|
|
}
|
|
],
|
|
"agent_config": {
|
|
"max_steps": 15,
|
|
"initial_cash": 10000.0,
|
|
"stock_symbols": ["NVDA", "AAPL", "MSFT", "GOOGL", "META"]
|
|
},
|
|
"log_config": {
|
|
"log_path": "./data/agent_data"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Step 2: Run simulation**
|
|
|
|
```bash
|
|
# Make sure MCP services are running
|
|
cd agent_tools
|
|
python start_mcp_services.py &
|
|
cd ..
|
|
|
|
# Run simulation in DEV mode (no API costs)
|
|
DEPLOYMENT_MODE=DEV python main.py configs/position_tracking_test.json
|
|
```
|
|
|
|
**Step 3: Verify results via API**
|
|
|
|
```bash
|
|
# Start API server
|
|
uvicorn api.main:app --reload &
|
|
|
|
# Get job results
|
|
curl http://localhost:8000/api/v1/jobs | jq '.'
|
|
|
|
# Get positions for the job
|
|
JOB_ID="<job-id-from-above>"
|
|
curl "http://localhost:8000/api/v1/jobs/${JOB_ID}/positions?model=position-tracking-test" | jq '.'
|
|
```
|
|
|
|
**Step 4: Verify position continuity**
|
|
|
|
Check the output JSON:
|
|
1. **Cash continuity:** Each day's starting cash should equal previous day's ending cash
|
|
2. **Holdings persistence:** Stock positions should persist across days unless sold
|
|
3. **Profit accuracy:** Profit should be 0 when buying/selling (no price change), and non-zero only when prices move
|
|
|
|
**Step 5: Document test results**
|
|
|
|
If all looks good, create a commit:
|
|
|
|
```bash
|
|
git add configs/position_tracking_test.json
|
|
git commit -m "test: add manual test config for position tracking verification"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Final Verification and Cleanup
|
|
|
|
**Step 1: Run complete test suite**
|
|
|
|
```bash
|
|
./venv/bin/python -m pytest tests/ -v --cov=. --cov-report=term-missing --cov-report=html --tb=short
|
|
```
|
|
|
|
Expected: All tests PASS, coverage ≥ 90%
|
|
|
|
**Step 2: Check for any remaining references to deleted methods**
|
|
|
|
```bash
|
|
git grep -n "get_positions\|get_last_trade\|get_current_prices\|_write_results_to_db\|_calculate_portfolio_value" -- '*.py'
|
|
```
|
|
|
|
Expected: Only references in:
|
|
- Test files (mocking for legacy tests)
|
|
- Comments explaining the removal
|
|
- This plan document
|
|
|
|
If any production code references these, they need to be removed.
|
|
|
|
**Step 3: Clean up any debug prints or temporary code**
|
|
|
|
Review all modified files for:
|
|
- Debug `print()` statements
|
|
- Commented-out code
|
|
- TODO comments that should be addressed
|
|
|
|
**Step 4: Final commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "fix: complete position tracking bug fixes - all tests passing"
|
|
```
|
|
|
|
**Step 5: Create summary report**
|
|
|
|
Create `docs/plans/2025-11-03-position-tracking-fixes-summary.md`:
|
|
|
|
```markdown
|
|
# Position Tracking Bug Fixes - Summary
|
|
|
|
**Date:** 2025-11-03
|
|
|
|
**Issue:** Three critical bugs in position tracking system:
|
|
1. Cash reset to initial value each trading day
|
|
2. Positions lost over non-continuous trading days (weekends)
|
|
3. Profit calculations showing trades as losses
|
|
|
|
## Root Causes
|
|
|
|
1. **Bugs #1 & #2:** `ModelDayExecutor._write_results_to_db()` called non-existent methods (`get_positions()`, `get_last_trade()`, `get_current_prices()`) on BaseAgent, resulting in corrupt position records with `cash=0` and `holdings=[]`. When `get_current_position_from_db()` retrieved positions for the next day, it found these corrupt records, causing cash resets and position losses.
|
|
|
|
2. **Bug #3:** Profit calculation compared portfolio value to **previous day's final value** instead of **start-of-day value**. Since buying/selling stocks doesn't change total portfolio value (cash ↓, stock value ↑ equally), this showed trades as having profit=0 or small rounding errors.
|
|
|
|
## Solution
|
|
|
|
1. **Removed redundant method:** Deleted `ModelDayExecutor._write_results_to_db()` and `_calculate_portfolio_value()` entirely. Position tracking is now exclusively handled by trade tools (`buy()`, `sell()`) and `add_no_trade_record_to_db()`.
|
|
|
|
2. **Fixed profit calculation:** Changed all profit calculations to compare against start-of-day portfolio value (action_id=0) instead of previous day's final value. This accurately reflects gains/losses from price movements and strategic trading.
|
|
|
|
## Files Modified
|
|
|
|
**Core Changes:**
|
|
- `api/model_day_executor.py`: Removed `_write_results_to_db()` call and method definitions
|
|
- `agent_tools/tool_trade.py`: Fixed profit calculation in `_buy_impl()` and `_sell_impl()`
|
|
- `tools/price_tools.py`: Fixed profit calculation in `add_no_trade_record_to_db()`
|
|
|
|
**Test Changes:**
|
|
- `tests/unit/test_position_tracking_bugs.py`: New tests demonstrating and verifying fixes
|
|
- `tests/unit/test_model_day_executor.py`: Updated mock helper and data persistence tests
|
|
- `tests/unit/test_model_day_executor_reasoning.py`: Skipped test for removed method
|
|
- `tests/integration/test_position_tracking_e2e.py`: New end-to-end integration test
|
|
|
|
**Documentation:**
|
|
- `CHANGELOG.md`: Added fix notes
|
|
- `docs/developer/database-schema.md`: Updated profit calculation documentation
|
|
|
|
## Verification
|
|
|
|
- All unit tests pass
|
|
- All integration tests pass
|
|
- Manual testing with 5-day simulation confirms position continuity
|
|
- Profit calculations accurate
|
|
|
|
## Impact
|
|
|
|
**Before:**
|
|
- Cash reset to $10,000 each trading day
|
|
- Positions lost after weekends
|
|
- Trades showed as $0 profit (misleading)
|
|
|
|
**After:**
|
|
- Cash carries over correctly between days
|
|
- Positions persist indefinitely until sold
|
|
- Profit accurately reflects price movements and trading strategy
|
|
```
|
|
|
|
**Step 6: Final commit**
|
|
|
|
```bash
|
|
git add docs/plans/2025-11-03-position-tracking-fixes-summary.md
|
|
git commit -m "docs: add summary report for position tracking bug fixes"
|
|
```
|
|
|
|
---
|
|
|
|
## Success Criteria
|
|
|
|
**All of the following must be true:**
|
|
|
|
✅ All tests in `tests/unit/test_position_tracking_bugs.py` PASS
|
|
✅ All existing unit tests continue to PASS
|
|
✅ Integration test demonstrates position continuity across 5 days
|
|
✅ Manual testing shows correct cash carry-over
|
|
✅ Manual testing shows positions persist over weekends
|
|
✅ Manual testing shows profit=0 for trades without price changes
|
|
✅ Code coverage maintained at ≥ 90%
|
|
✅ No references to deleted methods in production code
|
|
✅ Documentation updated
|
|
✅ CHANGELOG updated
|
|
|
|
---
|
|
|
|
## Rollback Plan
|
|
|
|
If issues are discovered after deployment:
|
|
|
|
1. **Revert commits:**
|
|
```bash
|
|
git revert <commit-hash-range>
|
|
```
|
|
|
|
2. **Re-run tests to verify rollback:**
|
|
```bash
|
|
./venv/bin/python -m pytest tests/ -v
|
|
```
|
|
|
|
3. **Investigate root cause of rollback:**
|
|
- Check logs
|
|
- Review test failures
|
|
- Identify what was missed
|
|
|
|
4. **Create new fix with additional tests**
|
|
|
|
---
|
|
|
|
## Notes for Future Maintainers
|
|
|
|
**Position Tracking Architecture:**
|
|
|
|
- **Single source of truth:** Trade tools (`buy()`, `sell()`) and `add_no_trade_record_to_db()` are the ONLY functions that write position records.
|
|
|
|
- **ModelDayExecutor responsibilities:**
|
|
- Create initial position (action_id=0) on first day via `_initialize_starting_position()`
|
|
- Manage trading sessions and reasoning logs
|
|
- **Does NOT** write position records directly
|
|
|
|
- **Action ID sequence:**
|
|
- `action_id=0`: Start-of-day baseline (created once, used for profit calculations)
|
|
- `action_id=1+`: Incremented for each trade or no-trade action
|
|
|
|
- **Profit calculation:**
|
|
- Always compare to start-of-day (action_id=0) portfolio value
|
|
- Never compare to previous day's final value
|
|
- This ensures trades don't show false profits/losses
|
|
|
|
**Testing:**
|
|
|
|
- `test_position_tracking_bugs.py` contains regression tests for these specific bugs
|
|
- If modifying position tracking, run these tests first
|
|
- Integration test (`test_position_tracking_e2e.py`) verifies multi-day continuity |