Skip to content

feat: portfolio performance analytics (Sharpe, Sortino, Max DD, Win Rate)#1

Open
biswajeetdev wants to merge 2 commits into
mainfrom
worktree-sparkling-mapping-feather
Open

feat: portfolio performance analytics (Sharpe, Sortino, Max DD, Win Rate)#1
biswajeetdev wants to merge 2 commits into
mainfrom
worktree-sparkling-mapping-feather

Conversation

@biswajeetdev

Copy link
Copy Markdown
Owner

Summary

  • Adds broker/performance.py — standalone analytics module that computes key performance metrics from local trade history and equity curve
  • Wires the report into the daily email summary (appended as a structured block every close-of-day)

Metrics computed

Metric Source
Sharpe Ratio equity curve (annualised, rf=5.5%)
Sortino Ratio equity curve (downside deviation only)
Max Drawdown % equity curve (peak-to-trough)
Win Rate % trade_history.json
Profit Factor gross profit / gross loss
Avg Win / Avg Loss trade_history.json
Avg Hold Days entry_date → exit_date per trade
Total & Last-20 PnL trade_history.json

Design notes

  • All functions are pure-safe — return None for metrics that need more data points (min 20 equity observations for ratio calculations) rather than crashing
  • get_performance_report() is the single public entry point; returns a flat dict safe to log/serialise
  • format_for_summary() produces a fixed-width text block for Telegram / email
  • Module is independently runnable: python -m broker.performance

Test plan

  • python -m broker.performance prints N/A gracefully when trade_history.json is empty
  • Daily email received at market close includes performance block
  • Sharpe / Sortino return None (not crash) with fewer than 20 equity data points

Adds broker/performance.py with Sharpe, Sortino, Max Drawdown,
Win Rate, Profit Factor, and Avg Hold Time computed from
trade_history.json and equity curve. Wires the report block
into the daily email summary so every close-of-day email
includes a structured performance snapshot.

@biswajeetdev biswajeetdev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review pass — a few items to fix before this is production-ready.

Comment thread broker/performance.py
if len(equity) < 2:
return np.array([])
arr = np.array(equity, dtype=float)
return np.diff(arr) / arr[:-1]

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero-division risk. If any equity snapshot is 0.0 (e.g. before the first deposit), arr[:-1] contains a zero and np.diff(arr) / arr[:-1] silently produces inf/nan, which propagates through Sharpe/Sortino giving garbage ratios.

Fix: strip zero snapshots before computing returns:

arr = arr[arr != 0]
if len(arr) < 2:
    return np.array([])
return np.diff(arr) / arr[:-1]

Comment thread broker/performance.py Outdated
return np.diff(arr) / arr[:-1]


def sharpe_ratio(equity: list[float]) -> float | None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3.10+ syntax. float | None union shorthand requires Python 3.10+. Use Optional[float] from typing for 3.8/3.9 compatibility — applies to sharpe_ratio, sortino_ratio, and avg_hold_days.

Comment thread broker/performance.py Outdated
losses = [t["pnl"] for t in closed if t["pnl"] <= 0]

gross_profit = sum(wins)
gross_loss = abs(sum(losses)) or 1e-9

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading profit factor on perfect win rate. abs(sum(losses)) or 1e-9 returns 1e-9 when there are no losing trades, making profit_factor astronomically large (e.g. 4.2e+11) in the Telegram/email output.

Fix:

gross_loss = abs(sum(losses))
profit_factor = round(gross_profit / gross_loss, 3) if gross_loss > 0 else None

The fmt() helper in format_for_summary already handles None gracefully.

Comment thread broker/performance.py Outdated
entry = datetime.fromisoformat(t["entry_date"])
exit_ = datetime.fromisoformat(t.get("exit_date") or t.get("close_date") or "")
durations.append((exit_ - entry).days)
except Exception:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent data loss. except Exception: continue swallows KeyError on trade records missing entry_date, dropping corrupted entries with no visibility.

Add a log line at minimum:

except Exception as e:
    print(f"[perf] skipping trade in avg_hold_days: {e}")
    continue

…tor, logging

- Strip zero equity snapshots before computing daily returns to prevent inf/nan
- Replace float|None union syntax with Optional[float] for Python 3.8/3.9 compat
- Fix profit_factor: return None (not 1e-9 floor) when there are no losing trades
- Log skipped trades in avg_hold_days instead of silently dropping them
@biswajeetdev

Copy link
Copy Markdown
Owner Author

Addressed all four review points in the follow-up commit (0b3f016):

  • Zero-div guard: zero equity snapshots stripped before diff computation
  • Optional types: replaced float | None with Optional[float] from typing across all three functions
  • Profit factor: now returns None on perfect win rate instead of 1e-9 floor
  • Exception logging: avg_hold_days now prints a [perf] line per skipped trade

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant