Skip to content

Commit ee1d434

Browse files
Deevhyne1023collinsezedike
authored andcommitted
fix: unify maxDrawdown sign convention
1 parent 55b4b97 commit ee1d434

3 files changed

Lines changed: 14 additions & 11 deletions

File tree

‎packages/strategies/src/report.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,8 @@ export function mergeReports(
508508
// continuation's volatility / sharpeProxy, since the continuation reflects
509509
// the most recent regime. Callers that need a mathematically combined
510510
// metric should recompute from raw timeseries; this merge is best-effort.
511+
// `maxDrawdown` is a non-negative ratio (0 = no drawdown, 1 = total loss),
512+
// so the larger value is the more severe drawdown.
511513
const maxDrawdown =
512514
base.riskMetrics.maxDrawdown.compareTo(
513515
continuation.riskMetrics.maxDrawdown

‎packages/strategies/src/risk-metrics.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,10 @@ describe("valueAtRisk", () => {
153153
});
154154

155155
describe("maxDrawdown", () => {
156-
it("returns the most negative entry", () => {
156+
it("returns the largest decline as a non-negative fraction", () => {
157157
const worst = maxDrawdown(series("0", "-0.02", "-0.05", "-0.01"));
158158

159-
expect(worst.toString()).toBe("-0.050000000000000000");
159+
expect(worst.toString()).toBe("0.050000000000000000");
160160
});
161161

162162
it("returns zero for an empty series", () => {
@@ -171,7 +171,7 @@ describe("maxDrawdown", () => {
171171

172172
it("returns the only entry of a single-element series", () => {
173173
expect(maxDrawdown(series("-0.03")).toString()).toBe(
174-
"-0.030000000000000000"
174+
"0.030000000000000000"
175175
);
176176
});
177177
});
@@ -186,7 +186,7 @@ describe("computeRiskMetrics", () => {
186186
confidenceLevel: 0.95,
187187
});
188188

189-
expect(metrics.maxDrawdown.toString()).toBe("-0.050000000000000000");
189+
expect(metrics.maxDrawdown.toString()).toBe("0.050000000000000000");
190190
expect(metrics.sharpeRatio.toString()).toBe("-0.200000000000000000");
191191
expect(metrics.valueAtRisk.toString()).toBe("0.050000000000000000");
192192
});

‎packages/strategies/src/risk-metrics.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ import { Decimal } from "./decimal";
1616
* - `sharpeRatio` returns the per-period ratio. `annualizeSharpe` scales it by
1717
* `sqrt(periodsPerYear)`, the convention for i.i.d. periods. Use 365 for a
1818
* daily stablecoin-vault series, 52 weekly, 12 monthly.
19-
* - `maxDrawdown` is a signed decline, so it is 0 or lower. `valueAtRisk` is a
20-
* loss magnitude, so a positive result is a loss. The two signs differ by
21-
* intent and each is documented on its own function.
19+
* - `maxDrawdown` is a non-negative magnitude, so it is 0 or higher.
20+
* `valueAtRisk` is also a loss magnitude, so a positive result is a loss.
21+
* Both are quoted as positive losses, matching the report field convention.
2222
*/
2323

2424
/** Scale used for ratio arithmetic. Stroops are too coarse for a variance. */
2525
const METRICS_SCALE = 18;
2626

2727
export interface RiskMetrics {
28-
/** Most negative entry of the drawdown series, 0 or lower. */
28+
/** Most negative entry of the drawdown series as a magnitude, 0 or higher. */
2929
readonly maxDrawdown: Decimal;
3030
/** Per-period Sharpe ratio. Annualize with `annualizeSharpe`. */
3131
readonly sharpeRatio: Decimal;
@@ -134,8 +134,9 @@ export function valueAtRisk(
134134

135135
/**
136136
* Most negative entry of the drawdown series, where each entry is the running
137-
* `value / peak - 1` decline. An empty series, and a series that never leaves
138-
* its running peak, both give `0`.
137+
* `value / peak - 1` decline. The result is reported as a non-negative
138+
* magnitude: the sign of the worst decline is flipped, so a series that never
139+
* leaves its running peak gives `0`. An empty series also gives `0`.
139140
*/
140141
export function maxDrawdown(drawdown: readonly Decimal[]): Decimal {
141142
const scale = workingScale(drawdown);
@@ -145,7 +146,7 @@ export function maxDrawdown(drawdown: readonly Decimal[]): Decimal {
145146
worst = value.rescale(scale);
146147
}
147148
}
148-
return worst;
149+
return worst.neg();
149150
}
150151

151152
/** Computes all three metrics from the collector's series in one call. */

0 commit comments

Comments
 (0)