NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Trading Analytics for Laravel

Expectancy, drawdown & risk ratios

Expectancy

expectancy is the expected P&L of an average closed trade: win rate × average win − loss rate × average loss, based on gross P&L. A break-even trade is neither a win nor a loss — it only dilutes both rates — so the value is computed straight from the realized gross totals, (gross profit − gross loss) / closed trades, with no rounded rate in between:

$e = $analytics->expectancy;

$e->value;            // '266.66' — (winRate * averageWin) - (lossRate * averageLoss)
$e->averageWin;       // '550.00' — average gross profit of a winner
$e->averageLoss;      // '300.00' — average gross loss of a loser, as a positive amount
$e->winRate;          // '0.6666'
$e->lossRate;         // '0.3333'
$e->winningTrades;    // 2
$e->losingTrades;     // 1
$e->breakEvenTrades;  // 0 — closed at a P&L of exactly 0

Risk-reward ratio

riskRewardRatio divides the average win by the average loss, at full precision, truncated to 4 decimals only as the result; the average win and loss follow the run’s scale. A break-even trade does not dilute the average loss. The ratio stays 0.0000 while there are no winning or no losing trades:

$rr = $analytics->riskRewardRatio;

$rr->value;         // '1.8333' — averageWin / averageLoss
$rr->averageWin;    // '550.00' — at the run's scale
$rr->averageLoss;   // '300.00'

Maximum drawdown

maxDrawdown follows the equity curve of the closed trades in close-time order — the running sum of net P&L, starting at zero — and records the largest drop from a peak. The percentage is relative to that peak, divided at full precision, and is only set once equity has been positive:

$dd = $analytics->maxDrawdown;

$dd->value;        // '306.00' — the largest drop from a peak
$dd->percentage;   // '30.9090' — that drop as a percent of the peak
$dd->equity;       // '781.00' — final equity: the sum of net realized P&L
$dd->peak;         // '990.00' — the highest equity reached

Sharpe and Sortino

riskAdjustedReturns takes each closed trade’s net return (as a fraction of the position value) and, after the pass, derives the mean, the population standard deviation and the downside deviation. Sharpe divides the mean excess return by the standard deviation and Sortino by the downside deviation. Both are per-trade ratios — not annualized — with a risk-free rate of 0, and a ratio stays 0.0000 when its deviation is zero:

$r = $analytics->riskAdjustedReturns;

$r->sharpeRatio;         // '0.2271' — mean excess return / standard deviation
$r->sortinoRatio;        // '0.3372' — mean excess return / downside deviation
$r->meanReturn;          // '0.0099301075' — mean net return per trade, as a fraction
$r->standardDeviation;   // '0.0437207547' — population standard deviation of the returns
$r->downsideDeviation;   // '0.0294448637' — deviation of the returns below the risk-free rate
$r->riskFreeRate;        // '0.0000000000'
$r->sampleSize;          // 3 — one return per closed trade, folded into running sums

No return series is stored: each realized return is folded into running sums (count, sum, sum of squares, sum of squared shortfalls) during the pass, and the deviations are derived from those sums afterwards — so memory stays constant however long the history is.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.