Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .squad/agents/dinesh/charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Dinesh — Tester

## Role
Tester and quality assurance. Writes tests, validates edge cases, ensures correctness.

## Boundaries
- Owns all test files and test scenarios
- Uses unittest.TestCase (NOT pytest style)
- pytest is only used as the test runner (`poetry run pytest`)
- Does NOT implement production code — Richard handles that
- Reviews test coverage and identifies missing edge cases

## Project Context
- **Project:** investing-algorithm-framework (Python)
- **Stack:** Python, backtesting, portfolio management, algorithmic trading
- **User:** marcvanduyn
- **Test framework:** unittest (not pytest). Tests use unittest.TestCase classes.
15 changes: 15 additions & 0 deletions .squad/agents/dinesh/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Dinesh — History

## Learnings
- Project initialized: investing-algorithm-framework, Python algorithmic trading framework
- User: marcvanduyn
- Test convention: unittest.TestCase, pytest as runner only
- Drawdown metrics live in `investing_algorithm_framework/services/metrics/drawdown.py`
- Tests at `tests/services/metrics/test_drawdowns.py` — 18 tests across 4 test classes
- `get_equity_curve()` does NOT sort snapshots by timestamp — relies on input order
- `get_max_daily_drawdown()` uses pandas resample('1D').last() + pct_change() for daily returns
- `get_max_drawdown_duration()` tracks drawdown via (timestamp - drawdown_start).days
- Edge case bug found: `get_max_daily_drawdown` returns abs(min(daily_returns)) even when all returns are positive — needs clamping to negative returns only. Filed in decisions inbox for Richard.
- Mock snapshots need `created_at` (datetime) and `total_value` (float) attributes
- Positive-only returns edge case was fixed by Coordinator via `min(daily_returns.min(), 0)` clamping — all 18 tests now pass.
- Richard confirmed: `get_equity_curve` sorts by `created_at`, `get_max_daily_drawdown` uses `pct_change()`, `get_max_drawdown_duration` uses `.days` on timestamp diffs.
16 changes: 16 additions & 0 deletions .squad/agents/gilfoyle/charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Gilfoyle — Lead

## Role
Lead engineer. Scope decisions, architecture review, code review, quality gates.

## Boundaries
- Owns architecture decisions and code review
- Does NOT implement features directly — delegates to Richard
- Does NOT write tests — delegates to Dinesh
- Reviews all significant changes before merge

## Project Context
- **Project:** investing-algorithm-framework (Python)
- **Stack:** Python, backtesting, portfolio management, algorithmic trading
- **User:** marcvanduyn
- **Test framework:** unittest (not pytest). Tests use unittest.TestCase classes.
5 changes: 5 additions & 0 deletions .squad/agents/gilfoyle/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Gilfoyle — History

## Learnings
- Project initialized: investing-algorithm-framework, Python algorithmic trading framework
- User: marcvanduyn
16 changes: 16 additions & 0 deletions .squad/agents/richard/charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Richard — Backend Dev

## Role
Backend developer. Python implementation, algorithms, data processing, backtesting logic, metrics computation.

## Boundaries
- Owns all Python implementation: services, domain logic, infrastructure
- Works on backtesting, portfolio management, metrics, data providers
- Does NOT review own code — Gilfoyle reviews
- Does NOT write test files — Dinesh handles tests

## Project Context
- **Project:** investing-algorithm-framework (Python)
- **Stack:** Python, backtesting, portfolio management, algorithmic trading
- **User:** marcvanduyn
- **Test framework:** unittest (not pytest). Tests use unittest.TestCase classes.
13 changes: 13 additions & 0 deletions .squad/agents/richard/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Richard — History

## Learnings
- Project initialized: investing-algorithm-framework, Python algorithmic trading framework
- User: marcvanduyn
- **Drawdown metrics (2026-03-29):** Fixed 3 bugs in `services/metrics/drawdown.py` and `services/metrics/equity_curve.py`.
- `get_equity_curve` must sort by `created_at` — all drawdown functions depend on it for peak-tracking.
- `get_max_daily_drawdown` should use `pct_change()` for worst single-day decline, not peak-to-trough.
- `get_max_drawdown_duration` must diff actual timestamps (`.days`), not count snapshot entries.
- Existing tests only cover `get_drawdown_series`, `get_max_drawdown`, `get_max_drawdown_absolute` — the three fixed functions need tests added by Dinesh.
- Test runner: `python3 -m pytest` (not `python`).
- Edge case: `get_max_daily_drawdown` must clamp to negative returns only — `min(daily_returns.min(), 0)`. Found by Dinesh during testing.
- Dinesh wrote 18 tests covering all fixed functions at `tests/services/metrics/test_drawdowns.py`.
16 changes: 16 additions & 0 deletions .squad/agents/scribe/charter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Scribe

## Role
Silent session logger. Maintains decisions.md, cross-agent context, orchestration logs.

## Boundaries
- Writes orchestration logs, session logs, decision merges
- Never speaks to the user
- Commits .squad/ state via git

## Tasks
1. Merge decisions inbox → decisions.md
2. Write orchestration log entries
3. Write session log entries
4. Cross-pollinate learnings to agent history files
5. Git commit .squad/ state
5 changes: 5 additions & 0 deletions .squad/agents/scribe/history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Scribe — History

## Learnings
- Project initialized: investing-algorithm-framework
- User: marcvanduyn
10 changes: 10 additions & 0 deletions .squad/casting/history.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"assignments": [
{
"assignment_id": "init-2026-03-29",
"universe": "silicon-valley",
"timestamp": "2026-03-29T00:00:00Z",
"agents": ["Gilfoyle", "Richard", "Dinesh"]
}
]
}
6 changes: 6 additions & 0 deletions .squad/casting/policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"version": 1,
"universe_allowlist": ["silicon-valley"],
"max_capacity": 10,
"overflow_strategy": "diegetic-expansion"
}
26 changes: 26 additions & 0 deletions .squad/casting/registry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[
{
"persistent_name": "Gilfoyle",
"role": "Lead",
"universe": "silicon-valley",
"created_at": "2026-03-29T00:00:00Z",
"legacy_named": false,
"status": "active"
},
{
"persistent_name": "Richard",
"role": "Backend Dev",
"universe": "silicon-valley",
"created_at": "2026-03-29T00:00:00Z",
"legacy_named": false,
"status": "active"
},
{
"persistent_name": "Dinesh",
"role": "Tester",
"universe": "silicon-valley",
"created_at": "2026-03-29T00:00:00Z",
"legacy_named": false,
"status": "active"
}
]
16 changes: 16 additions & 0 deletions .squad/ceremonies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Ceremonies

## Design Review

- **type:** manual
- **facilitator:** Gilfoyle
- **participants:** [Richard, Dinesh]
- **trigger:** User requests "design review" or "architecture review"

## Code Review

- **type:** auto
- **when:** after
- **condition:** implementation complete
- **facilitator:** Gilfoyle
- **participants:** [Dinesh]
16 changes: 16 additions & 0 deletions .squad/decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Decisions

_Append-only ledger of team decisions._

### 2026-03-29T00:00:00Z: Fix drawdown metric bugs (#407)
**By:** Richard (Backend Dev)
**What:** Three bugs fixed in drawdown metrics:
1. `get_equity_curve` now sorts snapshots by `created_at` — all downstream metrics depend on chronological order.
2. `get_max_daily_drawdown` rewritten to compute worst single-day percentage decline (via `pct_change`) instead of peak-to-trough on daily data. This makes it distinct from `get_max_drawdown`.
3. `get_max_drawdown_duration` now computes elapsed calendar days between timestamps instead of counting snapshot entries.
**Why:** Metrics were inconsistent with portfolio snapshot data. `max_daily_drawdown == max_drawdown` was a symptom of both doing peak-to-trough. Duration was wrong for non-daily snapshot frequencies.

### 2026-03-29T00:00:00Z: Drawdown edge case — positive-only returns
**By:** Dinesh (Tester), fixed by Coordinator
**What:** `get_max_daily_drawdown` must clamp to negative returns only (`min(daily_returns.min(), 0)`). When all daily returns are positive, the function was returning the smallest positive return as a "drawdown."
**Why:** Bug found during test coverage work for issue #407.
17 changes: 17 additions & 0 deletions .squad/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Routing Rules

## Default Routes

| Pattern | Agent | Reason |
|---------|-------|--------|
| Architecture, design decisions, code review | Gilfoyle | Lead handles scope and quality |
| Python implementation, algorithms, data processing, backtesting, metrics | Richard | Backend handles core framework code |
| Tests, test cases, quality assurance, edge cases | Dinesh | Tester handles all test work |
| Logging, decisions, session tracking | Scribe | Scribe handles memory and docs |
| Work queue, backlog monitoring | Ralph | Ralph monitors work pipeline |

## Domain Keywords

- **Gilfoyle:** architecture, review, scope, decision, design, API design, refactor planning
- **Richard:** python, algorithm, backtest, portfolio, metrics, drawdown, orders, positions, data provider, services, infrastructure
- **Dinesh:** test, unittest, coverage, edge case, regression, validation, assertion
18 changes: 18 additions & 0 deletions .squad/team.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Squad Team

## Project Context

- **Project:** investing-algorithm-framework
- **Stack:** Python, backtesting, portfolio management, algorithmic trading
- **Description:** Open-source framework for creating and deploying investing algorithms with backtesting, portfolio management, and multi-exchange support
- **User:** marcvanduyn

## Members

| Name | Role | Scope | Badge |
|------|------|-------|-------|
| Gilfoyle | Lead | Scope, decisions, code review | 🏗️ Lead |
| Richard | Backend Dev | Python, algorithms, data processing | 🔧 Backend |
| Dinesh | Tester | Tests, quality, edge cases | 🧪 Tester |
| Scribe | Session Logger | Memory, decisions, session logs | 📋 Scribe |
| Ralph | Work Monitor | Work queue, backlog, keep-alive | 🔄 Monitor |
59 changes: 32 additions & 27 deletions investing_algorithm_framework/services/metrics/drawdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,17 @@ def get_max_drawdown(snapshots: List[PortfolioSnapshot]) -> float:

def get_max_daily_drawdown(snapshots: List[PortfolioSnapshot]) -> float:
"""
Calculate the maximum daily drawdown of the portfolio as a percentage from the peak.
Calculate the worst single-day decline of the portfolio as a percentage.

This is the largest drop in equity (in percentage) from a peak to a trough
during the backtest period, calculated on a daily basis.
This is the largest day-over-day percentage drop in equity,
NOT the peak-to-trough drawdown (use get_max_drawdown for that).

Args:
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots

Returns:
float: The maximum daily drawdown as a negative percentage (e.g., -5.0 for a 5% drawdown).
float: The maximum single-day drawdown as a positive percentage
(e.g., 0.05 for a 5% single-day decline).
"""
# Create DataFrame from snapshots
data = [(s.created_at, s.total_value) for s in snapshots]
Expand All @@ -136,54 +137,58 @@ def get_max_daily_drawdown(snapshots: List[PortfolioSnapshot]) -> float:
# Filter out non-positive values
positive_values = daily_df[daily_df['total_value'] > 0]['total_value']

if positive_values.empty:
if positive_values.empty or len(positive_values) < 2:
return 0.0

peak = positive_values.iloc[0]
max_daily_drawdown_pct = 0.0
# Compute day-over-day returns; the worst single-day decline
# is the most negative return (ignore positive returns)
daily_returns = positive_values.pct_change().dropna()
negative_returns = daily_returns[daily_returns < 0]

for equity in positive_values:
if equity > peak:
peak = equity

# Avoid division by zero (shouldn't happen but extra safety)
if peak <= 0:
continue

drawdown_pct = (equity - peak) / peak
max_daily_drawdown_pct = min(max_daily_drawdown_pct, drawdown_pct)
if negative_returns.empty:
return 0.0

return abs(max_daily_drawdown_pct) # Return as positive percentage
return abs(negative_returns.min())

def get_max_drawdown_duration(snapshots: List[PortfolioSnapshot]) -> int:
"""
Calculate the maximum duration of drawdown in days.

This is the longest period where the portfolio equity was below its peak.
This is the longest period (in calendar days) where the portfolio
equity was below its peak.

Args:
snapshots (List[PortfolioSnapshot]): List of portfolio snapshots

Returns:
int: The maximum drawdown duration in days.
int: The maximum drawdown duration in calendar days.
"""
equity_curve = get_equity_curve(snapshots)
if not equity_curve:
return 0

peak = equity_curve[0][0]
max_duration = 0
current_duration = 0
drawdown_start = None

for equity, _ in equity_curve:
for equity, timestamp in equity_curve:
if equity < peak:
current_duration += 1
# Entering or continuing a drawdown
if drawdown_start is None:
drawdown_start = timestamp
else:
max_duration = max(max_duration, current_duration)
current_duration = 0
peak = equity # Reset peak to current equity
# Recovered to or above the peak
if drawdown_start is not None:
elapsed = (timestamp - drawdown_start).days
max_duration = max(max_duration, elapsed)
drawdown_start = None
peak = equity

max_duration = max(max_duration, current_duration) # Final check
# If still in drawdown at the end of the series
if drawdown_start is not None and len(equity_curve) > 0:
last_timestamp = equity_curve[-1][1]
elapsed = (last_timestamp - drawdown_start).days
max_duration = max(max_duration, elapsed)

return max_duration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,7 @@ def get_equity_curve(
total_size = snapshot.total_value
series.append((total_size, timestamp))

# Sort by timestamp to ensure chronological order
series.sort(key=lambda x: x[1])

return series
Loading
Loading