openalgo-script 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +929 -0
  2. package/README.md +69 -16
  3. package/dist/adapters/charts/driving.d.ts +50 -0
  4. package/dist/adapters/charts/driving.d.ts.map +1 -0
  5. package/dist/adapters/charts/driving.js +57 -0
  6. package/dist/adapters/charts/driving.js.map +1 -0
  7. package/dist/adapters/charts/run.d.ts +20 -0
  8. package/dist/adapters/charts/run.d.ts.map +1 -1
  9. package/dist/adapters/charts/run.js +83 -16
  10. package/dist/adapters/charts/run.js.map +1 -1
  11. package/dist/adapters/charts/venue.d.ts +73 -0
  12. package/dist/adapters/charts/venue.d.ts.map +1 -0
  13. package/dist/adapters/charts/venue.js +104 -0
  14. package/dist/adapters/charts/venue.js.map +1 -0
  15. package/dist/core/accounting/analysis.d.ts +111 -0
  16. package/dist/core/accounting/analysis.d.ts.map +1 -0
  17. package/dist/core/accounting/analysis.js +123 -0
  18. package/dist/core/accounting/analysis.js.map +1 -0
  19. package/dist/core/accounting/equity.d.ts +33 -0
  20. package/dist/core/accounting/equity.d.ts.map +1 -1
  21. package/dist/core/accounting/equity.js +12 -0
  22. package/dist/core/accounting/equity.js.map +1 -1
  23. package/dist/core/accounting/index.d.ts +2 -0
  24. package/dist/core/accounting/index.d.ts.map +1 -1
  25. package/dist/core/accounting/index.js +1 -0
  26. package/dist/core/accounting/index.js.map +1 -1
  27. package/dist/core/accounting/report.d.ts +3 -0
  28. package/dist/core/accounting/report.d.ts.map +1 -1
  29. package/dist/core/accounting/report.js +2 -0
  30. package/dist/core/accounting/report.js.map +1 -1
  31. package/dist/core/accounting/statistics.d.ts +12 -0
  32. package/dist/core/accounting/statistics.d.ts.map +1 -1
  33. package/dist/core/accounting/statistics.js +23 -1
  34. package/dist/core/accounting/statistics.js.map +1 -1
  35. package/dist/core/backtest/case.d.ts +60 -0
  36. package/dist/core/backtest/case.d.ts.map +1 -0
  37. package/dist/core/backtest/case.js +319 -0
  38. package/dist/core/backtest/case.js.map +1 -0
  39. package/dist/core/backtest/compare.d.ts.map +1 -1
  40. package/dist/core/backtest/compare.js +2 -0
  41. package/dist/core/backtest/compare.js.map +1 -1
  42. package/dist/core/backtest/deliver.d.ts +93 -0
  43. package/dist/core/backtest/deliver.d.ts.map +1 -0
  44. package/dist/core/backtest/deliver.js +94 -0
  45. package/dist/core/backtest/deliver.js.map +1 -0
  46. package/dist/core/backtest/drive.d.ts +66 -8
  47. package/dist/core/backtest/drive.d.ts.map +1 -1
  48. package/dist/core/backtest/drive.js +109 -59
  49. package/dist/core/backtest/drive.js.map +1 -1
  50. package/dist/core/backtest/index.d.ts +24 -12
  51. package/dist/core/backtest/index.d.ts.map +1 -1
  52. package/dist/core/backtest/index.js +21 -10
  53. package/dist/core/backtest/index.js.map +1 -1
  54. package/dist/core/backtest/record.d.ts +63 -2
  55. package/dist/core/backtest/record.d.ts.map +1 -1
  56. package/dist/core/backtest/record.js +71 -4
  57. package/dist/core/backtest/record.js.map +1 -1
  58. package/dist/core/backtest/replay.d.ts.map +1 -1
  59. package/dist/core/backtest/replay.js +11 -1
  60. package/dist/core/backtest/replay.js.map +1 -1
  61. package/dist/core/backtest/simulate.d.ts +113 -1
  62. package/dist/core/backtest/simulate.d.ts.map +1 -1
  63. package/dist/core/backtest/simulate.js +123 -5
  64. package/dist/core/backtest/simulate.js.map +1 -1
  65. package/dist/core/catalogue/catalogue.generated.d.ts +1 -1
  66. package/dist/core/catalogue/catalogue.generated.js +1 -1
  67. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  68. package/dist/core/check/library-orders.js +2 -2
  69. package/dist/core/check/library-orders.js.map +1 -1
  70. package/dist/core/check/library-prose.generated.js +2 -2
  71. package/dist/core/check/library-prose.generated.js.map +1 -1
  72. package/dist/core/emit/canonical.d.ts +22 -8
  73. package/dist/core/emit/canonical.d.ts.map +1 -1
  74. package/dist/core/emit/canonical.js +67 -6
  75. package/dist/core/emit/canonical.js.map +1 -1
  76. package/dist/core/engine/arithmetic.d.ts +6 -25
  77. package/dist/core/engine/arithmetic.d.ts.map +1 -1
  78. package/dist/core/engine/arithmetic.js +48 -3
  79. package/dist/core/engine/arithmetic.js.map +1 -1
  80. package/dist/core/engine/index.d.ts +1 -1
  81. package/dist/core/engine/index.d.ts.map +1 -1
  82. package/dist/core/engine/index.js +1 -1
  83. package/dist/core/engine/index.js.map +1 -1
  84. package/dist/core/engine/library/arrays.d.ts.map +1 -1
  85. package/dist/core/engine/library/arrays.js +8 -2
  86. package/dist/core/engine/library/arrays.js.map +1 -1
  87. package/dist/core/engine/library/code-points.d.ts +40 -0
  88. package/dist/core/engine/library/code-points.d.ts.map +1 -0
  89. package/dist/core/engine/library/code-points.js +74 -0
  90. package/dist/core/engine/library/code-points.js.map +1 -0
  91. package/dist/core/engine/library/index.d.ts +5 -0
  92. package/dist/core/engine/library/index.d.ts.map +1 -1
  93. package/dist/core/engine/library/index.js +5 -0
  94. package/dist/core/engine/library/index.js.map +1 -1
  95. package/dist/core/engine/library/text.d.ts.map +1 -1
  96. package/dist/core/engine/library/text.js +51 -34
  97. package/dist/core/engine/library/text.js.map +1 -1
  98. package/dist/core/engine/load.d.ts +20 -0
  99. package/dist/core/engine/load.d.ts.map +1 -1
  100. package/dist/core/engine/load.js +62 -0
  101. package/dist/core/engine/load.js.map +1 -1
  102. package/dist/core/engine/verify-tables.d.ts.map +1 -1
  103. package/dist/core/engine/verify-tables.js +41 -0
  104. package/dist/core/engine/verify-tables.js.map +1 -1
  105. package/dist/core/index.d.ts +10 -5
  106. package/dist/core/index.d.ts.map +1 -1
  107. package/dist/core/index.js +8 -3
  108. package/dist/core/index.js.map +1 -1
  109. package/dist/core/stdlib/index.d.ts +1 -1
  110. package/dist/core/stdlib/index.d.ts.map +1 -1
  111. package/dist/core/stdlib/index.js +1 -1
  112. package/dist/core/stdlib/index.js.map +1 -1
  113. package/dist/core/stdlib/maths/index.d.ts +1 -1
  114. package/dist/core/stdlib/maths/index.d.ts.map +1 -1
  115. package/dist/core/stdlib/maths/index.js +1 -1
  116. package/dist/core/stdlib/maths/index.js.map +1 -1
  117. package/dist/core/stdlib/maths/rounding.d.ts +5 -0
  118. package/dist/core/stdlib/maths/rounding.d.ts.map +1 -1
  119. package/dist/core/stdlib/maths/rounding.js +19 -1
  120. package/dist/core/stdlib/maths/rounding.js.map +1 -1
  121. package/dist/core/version/version.generated.d.ts +1 -1
  122. package/dist/core/version/version.generated.js +1 -1
  123. package/package.json +14 -2
  124. package/spec/README.md +2 -1
  125. package/spec/errors.json +3 -3
  126. package/src/adapters/charts/driving.ts +109 -0
  127. package/src/adapters/charts/run.ts +120 -28
  128. package/src/adapters/charts/venue.ts +132 -0
  129. package/src/core/accounting/analysis.ts +188 -0
  130. package/src/core/accounting/equity.ts +38 -0
  131. package/src/core/accounting/index.ts +2 -0
  132. package/src/core/accounting/report.ts +5 -0
  133. package/src/core/accounting/statistics.ts +38 -1
  134. package/src/core/backtest/case.ts +395 -0
  135. package/src/core/backtest/compare.ts +2 -0
  136. package/src/core/backtest/deliver.ts +161 -0
  137. package/src/core/backtest/drive.ts +175 -71
  138. package/src/core/backtest/index.ts +24 -12
  139. package/src/core/backtest/record.ts +134 -5
  140. package/src/core/backtest/replay.ts +11 -1
  141. package/src/core/backtest/simulate.ts +201 -6
  142. package/src/core/catalogue/catalogue.generated.ts +1 -1
  143. package/src/core/check/library-orders.ts +2 -2
  144. package/src/core/check/library-prose.generated.ts +2 -2
  145. package/src/core/emit/canonical.ts +67 -9
  146. package/src/core/engine/arithmetic.ts +23 -3
  147. package/src/core/engine/index.ts +1 -1
  148. package/src/core/engine/library/arrays.ts +8 -2
  149. package/src/core/engine/library/code-points.ts +73 -0
  150. package/src/core/engine/library/index.ts +6 -0
  151. package/src/core/engine/library/text.ts +53 -35
  152. package/src/core/engine/load.ts +68 -0
  153. package/src/core/engine/verify-tables.ts +43 -0
  154. package/src/core/index.ts +16 -2
  155. package/src/core/stdlib/index.ts +1 -0
  156. package/src/core/stdlib/maths/index.ts +1 -0
  157. package/src/core/stdlib/maths/rounding.ts +23 -1
  158. package/src/core/version/version.generated.ts +1 -1
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The trades taken apart: by direction, by extreme, and by run.
3
+ *
4
+ * The summary answers what the whole run did. Three questions it cannot answer
5
+ * are asked here, and each of them is a question about whether the summary
6
+ * means what it looks like it means.
7
+ *
8
+ * **Which side made the money.** A run whose long trades paid for its short
9
+ * ones reports a healthy net and is two strategies, one of which is losing. The
10
+ * summary cannot show that, because every figure in it is folded over both
11
+ * sides at once. Splitting it is not a refinement of the headline number, it is
12
+ * the first thing that can contradict it.
13
+ *
14
+ * **Whether one trade is the result.** A hundred trades and a profit factor of
15
+ * two reads as an edge until the largest win is the whole of the net. The
16
+ * expectancy's standard error already says how wide the spread is; the largest
17
+ * win and the largest loss say where the width came from, which is the part a
18
+ * reader can act on.
19
+ *
20
+ * **What the run of losses was.** The deepest drawdown is a money figure and
21
+ * the longest one is a bar count, and neither is the number that actually stops
22
+ * somebody trading a strategy. That number is how many times in a row it was
23
+ * wrong. It is not derivable from anything in the summary: the same win rate
24
+ * over the same trades gives a streak of two or a streak of eleven depending on
25
+ * an ordering the summary folds away.
26
+ *
27
+ * ## Every figure here is over closed trades, and that is the whole rule
28
+ *
29
+ * An open trade has no net to win or lose by, so it is in none of these counts,
30
+ * on either side, in no streak and in no extreme. `statistics.ts` has three
31
+ * different answers to which trades a figure is counted over, because a summary
32
+ * carries a capital figure and a bar count that are facts about the run rather
33
+ * than about its trades. Nothing here is a fact about the run, so there is one
34
+ * answer, and a reader can hold it.
35
+ *
36
+ * The consequence worth stating: `longCount + shortCount` is `tradeCount` and
37
+ * not the length of the list folded. A run holding an open position reports
38
+ * fewer trades here than it has, and the summary's `openTradeCount` is where
39
+ * the difference is accounted for.
40
+ */
41
+
42
+ import type { Money } from './shapes.js';
43
+ import type { Trade } from './trades.js';
44
+
45
+ /** One direction's own figures, folded over that side's closed trades. */
46
+ export interface SideAnalysis {
47
+ readonly count: number;
48
+ readonly wins: number;
49
+ readonly losses: number;
50
+ /** Exactly zero net, counted in neither half, as in the summary. */
51
+ readonly scratches: number;
52
+ /** Net after charges, which is the figure the sides are compared on. */
53
+ readonly netProfit: Money;
54
+ /** `wins / (wins + losses)`, null where this side decided nothing. */
55
+ readonly winRate: number | null;
56
+ }
57
+
58
+ /**
59
+ * The trades by direction, by extreme and by run.
60
+ *
61
+ * `long` and `short` partition the closed trades, so their counts sum to the
62
+ * summary's `tradeCount` and their nets sum to its `netProfit`. That is
63
+ * asserted rather than assumed, because a partition that stops partitioning is
64
+ * the defect this shape exists to make visible and it would otherwise show up
65
+ * as two panels that quietly disagree.
66
+ */
67
+ export interface TradeAnalysis {
68
+ readonly long: SideAnalysis;
69
+ readonly short: SideAnalysis;
70
+ /**
71
+ * The best closed trade's net, zero where none closed and zero where every
72
+ * closed trade lost.
73
+ *
74
+ * Zero rather than null, and it is the one place here that is worth arguing
75
+ * about. A run whose every trade lost has no winning trade to name, and null
76
+ * would say so. It reports zero because the figure is read beside
77
+ * `largestLoss` and against `netProfit`, and a null in a column of money
78
+ * forces every reader of the report to handle a case that means "nothing
79
+ * won", which is what zero already means in this column.
80
+ */
81
+ readonly largestWin: Money;
82
+ /** The worst closed trade's net as a positive magnitude, zero where none lost. */
83
+ readonly largestLoss: Money;
84
+ /**
85
+ * The longest run of consecutive winning closed trades, in the order they
86
+ * closed.
87
+ *
88
+ * In the order they closed, and not the order they opened, because that is
89
+ * the order the account experienced them in and the streak is a fact about
90
+ * what the account experienced. The two orders differ whenever a trade is
91
+ * held across another one's whole life, which is every scaling strategy.
92
+ */
93
+ readonly maxConsecutiveWins: number;
94
+ /** The same, for losses. */
95
+ readonly maxConsecutiveLosses: number;
96
+ }
97
+
98
+ /** A side's figures under construction, before the rates are taken. */
99
+ interface SideTally {
100
+ count: number;
101
+ wins: number;
102
+ losses: number;
103
+ scratches: number;
104
+ netProfit: Money;
105
+ }
106
+
107
+ function emptySide(): SideTally {
108
+ return { count: 0, wins: 0, losses: 0, scratches: 0, netProfit: 0 };
109
+ }
110
+
111
+ function sideOf(tally: SideTally): SideAnalysis {
112
+ const decided = tally.wins + tally.losses;
113
+ return {
114
+ count: tally.count,
115
+ wins: tally.wins,
116
+ losses: tally.losses,
117
+ scratches: tally.scratches,
118
+ netProfit: tally.netProfit,
119
+ winRate: decided === 0 ? null : tally.wins / decided,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * One pass over the closed trades, in closing order.
125
+ *
126
+ * The list arrives in opening order, which is what the equity fold needs and
127
+ * what `tradesOf` produces. The streaks need closing order, so the closed
128
+ * trades are ordered here rather than anywhere else: reordering the list the
129
+ * caller holds would change the equity curve, and taking a second list in
130
+ * opening order and calling its streaks correct would be the defect this
131
+ * function is supposed to have caught.
132
+ *
133
+ * A trade whose net is exactly zero is a scratch, which is the summary's rule
134
+ * and is applied here for the same reason. A scratch **breaks** a streak
135
+ * without extending either one: a strategy that went right, flat, right was not
136
+ * right twice running, and counting the flat trade as either would make the
137
+ * streak a figure that depends on a rounding at the last digit.
138
+ */
139
+ export function analysisOf(trades: readonly Trade[]): TradeAnalysis {
140
+ const long = emptySide();
141
+ const short = emptySide();
142
+ let largestWin = 0;
143
+ let largestLoss = 0;
144
+ let maxConsecutiveWins = 0;
145
+ let maxConsecutiveLosses = 0;
146
+ let winStreak = 0;
147
+ let lossStreak = 0;
148
+
149
+ const closed = trades.filter((trade) => !trade.isOpen).slice();
150
+ // A trade closes on a bar, and two can close on the same one. The opening
151
+ // order breaks the tie, because it is the order the list arrived in and the
152
+ // only other fact available: a sort that is not total gives two engines two
153
+ // different streaks from one list of trades.
154
+ closed.sort((a, b) => (a.closedOnBar ?? 0) - (b.closedOnBar ?? 0) || a.index - b.index);
155
+
156
+ for (const trade of closed) {
157
+ const side = trade.side === 'long' ? long : short;
158
+ side.count += 1;
159
+ side.netProfit += trade.netProfit;
160
+
161
+ if (trade.netProfit > 0) {
162
+ side.wins += 1;
163
+ if (trade.netProfit > largestWin) largestWin = trade.netProfit;
164
+ winStreak += 1;
165
+ lossStreak = 0;
166
+ if (winStreak > maxConsecutiveWins) maxConsecutiveWins = winStreak;
167
+ } else if (trade.netProfit < 0) {
168
+ side.losses += 1;
169
+ if (-trade.netProfit > largestLoss) largestLoss = -trade.netProfit;
170
+ lossStreak += 1;
171
+ winStreak = 0;
172
+ if (lossStreak > maxConsecutiveLosses) maxConsecutiveLosses = lossStreak;
173
+ } else {
174
+ side.scratches += 1;
175
+ winStreak = 0;
176
+ lossStreak = 0;
177
+ }
178
+ }
179
+
180
+ return {
181
+ long: sideOf(long),
182
+ short: sideOf(short),
183
+ largestWin,
184
+ largestLoss,
185
+ maxConsecutiveWins,
186
+ maxConsecutiveLosses,
187
+ };
188
+ }
@@ -105,6 +105,33 @@ export interface EquityPoint {
105
105
  readonly drawdown: Money;
106
106
  /** `drawdown / runningPeak`, a fraction and not a figure times a hundred. */
107
107
  readonly drawdownPercent: number;
108
+ /**
109
+ * The distance above the running trough, zero or positive.
110
+ *
111
+ * Drawdown's mirror, and carried for the same reason drawdown is: it is the
112
+ * run's best stretch measured the same way its worst one is, so the two can
113
+ * be read against each other. A reader handed only the depth learns how bad
114
+ * it got and nothing about whether the run ever climbed far enough for that
115
+ * depth to be a giving back rather than a steady bleed.
116
+ */
117
+ readonly runUp: Money;
118
+ /**
119
+ * `runUp / runningTrough`, a fraction and not a figure times a hundred, and
120
+ * zero where the trough is not above zero.
121
+ *
122
+ * Not the same guard drawdown gets, because the two bases are not the same
123
+ * kind of number. The running peak starts at the capital and only ever rises,
124
+ * so a run with capital above zero has a peak above zero at every point. The
125
+ * running trough starts there and only ever falls, so an account that lost
126
+ * everything has a trough at or below zero, and from that bar on this figure
127
+ * is zero rather than a fraction. Zero is the wrong answer for a run that
128
+ * recovered, and it is reported anyway, because the alternative is a
129
+ * percentage against a negative basis: a positive climb reported as a
130
+ * negative fraction, which is the shape that once gave a profit factor of
131
+ * minus a half. `runUp` itself is unaffected and stays the figure to read on
132
+ * such a run.
133
+ */
134
+ readonly runUpPercent: number;
108
135
  }
109
136
 
110
137
  /**
@@ -150,6 +177,12 @@ export function ratioOf(value: number, basis: number): number {
150
177
  * The running peak starts at the capital rather than at the first point, so a
151
178
  * run that is down from its first bar is in drawdown at its first bar. Starting
152
179
  * it at the first point would report every run as having begun at its high.
180
+ *
181
+ * The running trough starts at the capital for the same reason and not for a
182
+ * symmetrical one: a run that is up from its first bar has run up from the
183
+ * money it was given, which is the figure a reader is measuring against. Anchor
184
+ * it at the first point instead and a run that gapped up on bar zero reports
185
+ * that gain as having come from nowhere.
153
186
  */
154
187
  export function equityOver(
155
188
  trades: readonly Trade[],
@@ -163,6 +196,7 @@ export function equityOver(
163
196
  let realised = 0;
164
197
  let charges = 0;
165
198
  let peak = capital;
199
+ let trough = capital;
166
200
  let mark: number | null = null;
167
201
 
168
202
  for (const bar of marks) {
@@ -208,7 +242,9 @@ export function equityOver(
208
242
  const cash = capital + realised - charges;
209
243
  const equity = cash + openProfit;
210
244
  if (equity > peak) peak = equity;
245
+ if (equity < trough) trough = equity;
211
246
  const drawdown = equity - peak;
247
+ const runUp = equity - trough;
212
248
  points.push({
213
249
  barIndex: bar.barIndex,
214
250
  time: bar.time,
@@ -220,6 +256,8 @@ export function equityOver(
220
256
  exposure,
221
257
  drawdown,
222
258
  drawdownPercent: ratioOf(drawdown, peak),
259
+ runUp,
260
+ runUpPercent: ratioOf(runUp, trough),
223
261
  });
224
262
  }
225
263
 
@@ -43,6 +43,8 @@ export { monthlyOver } from './monthly.js';
43
43
  export type { MonthlyReturn } from './monthly.js';
44
44
  export { markersOf } from './markers.js';
45
45
  export type { TradeMarker } from './markers.js';
46
+ export { analysisOf } from './analysis.js';
47
+ export type { SideAnalysis, TradeAnalysis } from './analysis.js';
46
48
  export { summaryOf } from './statistics.js';
47
49
  export type { Summary } from './statistics.js';
48
50
  export { reportOf } from './report.js';
@@ -19,6 +19,8 @@
19
19
  * structural half of keeping that decision open: when it is taken, the engine
20
20
  * calls this, and there is no second implementation to disagree with.
21
21
  */
22
+ import { analysisOf } from './analysis.js';
23
+ import type { TradeAnalysis } from './analysis.js';
22
24
  import { chargeFor } from './charges.js';
23
25
  import type { ChargeSchedule } from './charges.js';
24
26
  import { equityOver } from './equity.js';
@@ -36,6 +38,8 @@ import type { Trade } from './trades.js';
36
38
  /** Everything a run is reported as, and nothing a chart has to compute. */
37
39
  export interface Report {
38
40
  readonly summary: Summary;
41
+ /** The trades by direction, by extreme and by run. */
42
+ readonly analysis: TradeAnalysis;
39
43
  readonly trades: readonly Trade[];
40
44
  readonly equity: readonly EquityPoint[];
41
45
  readonly monthly: readonly MonthlyReturn[];
@@ -81,6 +85,7 @@ export function reportOf(
81
85
 
82
86
  return {
83
87
  summary,
88
+ analysis: analysisOf(trades),
84
89
  trades,
85
90
  equity,
86
91
  monthly: monthlyOver(equity, trades),
@@ -121,6 +121,18 @@ export interface Summary {
121
121
  /** A bar time, never an index. */
122
122
  readonly maxDrawdownAt: number | null;
123
123
  readonly longestDrawdownBars: number;
124
+ /**
125
+ * Zero or positive, the mirror of `maxDrawdown` and read against it.
126
+ *
127
+ * The pair is the point of it. A run that made ten and gave back nine is a
128
+ * different strategy from one that made one and never gave any of it back,
129
+ * and the two report the same net. Neither figure says that on its own.
130
+ */
131
+ readonly maxRunUp: Money;
132
+ /** The highest point's own fraction, not the best fraction of any point. */
133
+ readonly maxRunUpPercent: number;
134
+ /** A bar time, never an index, and not the bar `maxDrawdownAt` addresses. */
135
+ readonly maxRunUpAt: number | null;
124
136
  readonly averageBarsHeld: number | null;
125
137
  readonly barsInMarket: number;
126
138
  readonly barCount: number;
@@ -149,6 +161,9 @@ interface Depth {
149
161
  readonly maxDrawdownPercent: number;
150
162
  readonly maxDrawdownAt: number | null;
151
163
  readonly longestDrawdownBars: number;
164
+ readonly maxRunUp: Money;
165
+ readonly maxRunUpPercent: number;
166
+ readonly maxRunUpAt: number | null;
152
167
  }
153
168
 
154
169
  /**
@@ -199,6 +214,9 @@ export function summaryOf(
199
214
  maxDrawdownPercent: depth.maxDrawdownPercent,
200
215
  maxDrawdownAt: depth.maxDrawdownAt,
201
216
  longestDrawdownBars: depth.longestDrawdownBars,
217
+ maxRunUp: depth.maxRunUp,
218
+ maxRunUpPercent: depth.maxRunUpPercent,
219
+ maxRunUpAt: depth.maxRunUpAt,
202
220
  averageBarsHeld: tally.heldCount === 0 ? null : tally.heldTotal / tally.heldCount,
203
221
  barsInMarket: barsInMarketOver(trades, equity),
204
222
  barCount: equity.length,
@@ -303,6 +321,9 @@ function depthOf(equity: readonly EquityPoint[]): Depth {
303
321
  let maxDrawdownAt: number | null = null;
304
322
  let longestDrawdownBars = 0;
305
323
  let under = 0;
324
+ let maxRunUp = 0;
325
+ let maxRunUpPercent = 0;
326
+ let maxRunUpAt: number | null = null;
306
327
 
307
328
  for (const point of equity) {
308
329
  if (point.drawdown < maxDrawdown) {
@@ -316,9 +337,25 @@ function depthOf(equity: readonly EquityPoint[]): Depth {
316
337
  } else {
317
338
  under = 0;
318
339
  }
340
+ // Strictly greater, so the earliest bar reaching the height wins the tie,
341
+ // which is the rule the depth above is picked by. The two figures address
342
+ // different bars and each addresses the first bar that reached it.
343
+ if (point.runUp > maxRunUp) {
344
+ maxRunUp = point.runUp;
345
+ maxRunUpPercent = point.runUpPercent;
346
+ maxRunUpAt = point.time;
347
+ }
319
348
  }
320
349
 
321
- return { maxDrawdown, maxDrawdownPercent, maxDrawdownAt, longestDrawdownBars };
350
+ return {
351
+ maxDrawdown,
352
+ maxDrawdownPercent,
353
+ maxDrawdownAt,
354
+ longestDrawdownBars,
355
+ maxRunUp,
356
+ maxRunUpPercent,
357
+ maxRunUpAt,
358
+ };
322
359
  }
323
360
 
324
361
  /**