tradeblocks-mcp 3.0.2 → 3.0.3

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 (214) hide show
  1. package/README.md +82 -68
  2. package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
  3. package/dist/chunk-27S67XW3.js.map +1 -0
  4. package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
  5. package/dist/chunk-2E63THNI.js.map +1 -0
  6. package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
  7. package/dist/chunk-6S37CXUA.js.map +1 -0
  8. package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
  9. package/dist/chunk-AP7IHUUR.js.map +1 -0
  10. package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
  11. package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
  12. package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
  13. package/dist/chunk-SEUZYQGQ.js.map +1 -0
  14. package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
  15. package/dist/chunk-UBUC5A66.js.map +1 -0
  16. package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
  17. package/dist/chunk-VDU25Z6X.js.map +1 -0
  18. package/dist/daily-log-processor-BY3ISY6K.js +9 -0
  19. package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
  20. package/dist/iv-solver-worker.js +9 -1
  21. package/dist/market-provider-H3ARIVZ4.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
  24. package/dist/test-exports.js +358 -254
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-EYA3I3XB.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/manifest.json +3 -11
  29. package/package.json +1 -1
  30. package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
  31. package/server/chunk-4P7D7YZP.js.map +1 -0
  32. package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
  33. package/server/chunk-BOPHW5M6.js.map +1 -0
  34. package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
  35. package/server/chunk-GH2552SE.js.map +1 -0
  36. package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
  37. package/server/chunk-OBYKFW2B.js.map +1 -0
  38. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  39. package/server/chunk-T66KH2XH.js.map +1 -0
  40. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  41. package/server/chunk-W5E7FHC4.js.map +1 -0
  42. package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
  43. package/server/chunk-YUCOAJ4Z.js.map +1 -0
  44. package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
  45. package/server/chunk-ZBJCF4ZG.js.map +1 -0
  46. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  47. package/server/config-DK7KOMNL.js.map +1 -0
  48. package/server/daily-log-processor-ENEUT22A.js +10 -0
  49. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  50. package/server/http-server.js +2 -5
  51. package/server/index.js +1771 -1538
  52. package/server/index.js.map +1 -1
  53. package/server/iv-solver-worker.js +9 -1
  54. package/server/market-provider-B437HKLW.js +17 -0
  55. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  56. package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
  57. package/server/trade-processor-L3PIQ5TG.js +10 -0
  58. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  59. package/src/auth/clients-store.ts +4 -4
  60. package/src/auth/code-store.ts +1 -1
  61. package/src/auth/config.ts +11 -12
  62. package/src/auth/login-page.ts +8 -10
  63. package/src/auth/provider.ts +35 -30
  64. package/src/auth/token.ts +17 -15
  65. package/src/db/backtest-schemas.ts +12 -4
  66. package/src/db/connection.ts +107 -40
  67. package/src/db/index.ts +37 -5
  68. package/src/db/json-adapters.ts +22 -31
  69. package/src/db/json-migration.ts +11 -15
  70. package/src/db/json-store.ts +5 -3
  71. package/src/db/market-datasets.ts +48 -15
  72. package/src/db/market-schemas.ts +11 -5
  73. package/src/db/market-views.ts +119 -32
  74. package/src/db/parquet-writer.ts +7 -7
  75. package/src/db/profile-schemas.ts +39 -22
  76. package/src/db/schemas.ts +2 -2
  77. package/src/http-server.ts +7 -12
  78. package/src/index.ts +8 -9
  79. package/src/market/ingestor/index.ts +1 -4
  80. package/src/market/ingestor/market-ingestor.ts +140 -67
  81. package/src/market/ingestor/types.ts +1 -3
  82. package/src/market/stores/chain-sql.ts +2 -8
  83. package/src/market/stores/chain-store.ts +1 -4
  84. package/src/market/stores/duckdb-chain-store.ts +3 -14
  85. package/src/market/stores/duckdb-enriched-store.ts +3 -13
  86. package/src/market/stores/duckdb-quote-store.ts +10 -27
  87. package/src/market/stores/duckdb-spot-store.ts +4 -20
  88. package/src/market/stores/enriched-sql.ts +3 -9
  89. package/src/market/stores/enriched-store.ts +2 -2
  90. package/src/market/stores/parquet-chain-store.ts +5 -22
  91. package/src/market/stores/parquet-enriched-store.ts +4 -16
  92. package/src/market/stores/parquet-oi-daily-store.ts +3 -14
  93. package/src/market/stores/parquet-quote-store.ts +9 -29
  94. package/src/market/stores/parquet-spot-store.ts +14 -33
  95. package/src/market/stores/quote-store.ts +2 -10
  96. package/src/market/stores/spot-sql.ts +3 -15
  97. package/src/market/stores/spot-store.ts +1 -1
  98. package/src/market/tickers/defaults.json +6 -6
  99. package/src/market/tickers/loader.ts +2 -7
  100. package/src/market/tickers/registry.ts +5 -15
  101. package/src/market/tickers/schemas.ts +2 -6
  102. package/src/models/strategy-profile.ts +39 -39
  103. package/src/sync/block-sync.ts +44 -65
  104. package/src/sync/index.ts +1 -4
  105. package/src/sync/metadata.ts +14 -20
  106. package/src/test-exports.ts +127 -123
  107. package/src/tools/analysis.ts +180 -159
  108. package/src/tools/batch-exit-analysis.ts +100 -59
  109. package/src/tools/blocks/analysis.ts +116 -101
  110. package/src/tools/blocks/comparison.ts +281 -364
  111. package/src/tools/blocks/core.ts +208 -253
  112. package/src/tools/blocks/health.ts +571 -563
  113. package/src/tools/blocks/similarity.ts +316 -283
  114. package/src/tools/edge-decay.ts +124 -197
  115. package/src/tools/exit-analysis.ts +130 -77
  116. package/src/tools/greeks-attribution.ts +84 -35
  117. package/src/tools/guides.ts +4 -6
  118. package/src/tools/imports.ts +9 -12
  119. package/src/tools/market-data.ts +1067 -757
  120. package/src/tools/market-enrichment.ts +3 -3
  121. package/src/tools/market-fetch.ts +148 -67
  122. package/src/tools/market-imports.ts +12 -12
  123. package/src/tools/middleware/sync-middleware.ts +5 -6
  124. package/src/tools/performance.ts +185 -302
  125. package/src/tools/profile-analysis.ts +52 -66
  126. package/src/tools/profiles.ts +106 -69
  127. package/src/tools/regime-advisor.ts +20 -45
  128. package/src/tools/replay.ts +81 -77
  129. package/src/tools/reports/discrepancies.ts +298 -328
  130. package/src/tools/reports/fields.ts +7 -25
  131. package/src/tools/reports/helpers.ts +18 -49
  132. package/src/tools/reports/predictive.ts +27 -70
  133. package/src/tools/reports/slippage-trends.ts +315 -345
  134. package/src/tools/reports/slippage.ts +1 -4
  135. package/src/tools/reports/strategy-matches.ts +399 -441
  136. package/src/tools/schema.ts +43 -40
  137. package/src/tools/shared/filters.ts +3 -9
  138. package/src/tools/snapshot.ts +9 -30
  139. package/src/tools/sql.ts +15 -14
  140. package/src/tools/tickers.ts +1 -4
  141. package/src/utils/batch-exit-analysis.ts +31 -42
  142. package/src/utils/black-scholes.ts +39 -29
  143. package/src/utils/block-loader.ts +68 -82
  144. package/src/utils/calibration-probe.ts +1 -2
  145. package/src/utils/chain-loader.ts +3 -3
  146. package/src/utils/csv-discovery.ts +16 -22
  147. package/src/utils/data-quality.ts +24 -36
  148. package/src/utils/exit-triggers.ts +91 -96
  149. package/src/utils/field-timing.ts +94 -79
  150. package/src/utils/filter-predicates.ts +13 -9
  151. package/src/utils/flatfile-importer.ts +94 -64
  152. package/src/utils/greeks-decomposition.ts +152 -100
  153. package/src/utils/iv-solver-pool.ts +55 -25
  154. package/src/utils/iv-solver-worker.ts +5 -5
  155. package/src/utils/market-enricher.ts +528 -497
  156. package/src/utils/market-importer.ts +31 -12
  157. package/src/utils/market-provider.ts +21 -23
  158. package/src/utils/massive-tier.ts +5 -7
  159. package/src/utils/migrate-option-data-helpers.ts +2 -8
  160. package/src/utils/option-quote-greeks.ts +25 -31
  161. package/src/utils/option-time.ts +4 -8
  162. package/src/utils/output-formatter.ts +1 -4
  163. package/src/utils/provider-capabilities.ts +1 -4
  164. package/src/utils/providers/massive.ts +59 -93
  165. package/src/utils/providers/thetadata/backfill.ts +14 -23
  166. package/src/utils/providers/thetadata/client.ts +12 -8
  167. package/src/utils/providers/thetadata/decode.ts +2 -20
  168. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  169. package/src/utils/providers/thetadata/join.ts +11 -10
  170. package/src/utils/providers/thetadata/proto.ts +12 -10
  171. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  172. package/src/utils/providers/thetadata.ts +11 -10
  173. package/src/utils/quote-enricher.ts +4 -4
  174. package/src/utils/quote-parquet-projection.ts +3 -11
  175. package/src/utils/sample-date-selector.ts +3 -5
  176. package/src/utils/schema-metadata.ts +102 -70
  177. package/src/utils/ticker.ts +5 -9
  178. package/src/utils/trade-replay.ts +77 -68
  179. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  180. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  181. package/dist/chunk-FGZH632F.js.map +0 -1
  182. package/dist/chunk-LDKTV7GW.js.map +0 -1
  183. package/dist/chunk-PRAYH3RT.js.map +0 -1
  184. package/dist/chunk-W2PP3LEH.js.map +0 -1
  185. package/dist/chunk-XXYOUIZY.js.map +0 -1
  186. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  187. package/dist/market-provider-VDRJUEF2.js +0 -16
  188. package/dist/trade-processor-NHU4VWRX.js +0 -9
  189. package/server/chunk-5EBXHT6C.js.map +0 -1
  190. package/server/chunk-72GKJE2U.js.map +0 -1
  191. package/server/chunk-CSDVJPBB.js.map +0 -1
  192. package/server/chunk-FBNDMCT5.js.map +0 -1
  193. package/server/chunk-NRFXAJF7.js.map +0 -1
  194. package/server/chunk-NZO6PT64.js.map +0 -1
  195. package/server/chunk-PNKG7RY7.js.map +0 -1
  196. package/server/chunk-WA5AAPCH.js.map +0 -1
  197. package/server/config-6IZXEFEX.js.map +0 -1
  198. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  199. package/server/market-provider-VOYYVYWT.js +0 -17
  200. package/server/trade-processor-JWVS37KM.js +0 -10
  201. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
  202. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  203. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
  204. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  205. /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
  206. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
  207. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  208. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
  209. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  210. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
  211. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  212. /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
  213. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
  214. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -90,7 +90,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
90
90
  const hasPl = plColumnAliases.some((alias) => headers.includes(alias));
91
91
  // Match trade columns - require header to contain the full column pattern
92
92
  const matchedTradeColumns = tradeOptionalColumns.filter((col) =>
93
- headers.some((h) => h.includes(col))
93
+ headers.some((h) => h.includes(col)),
94
94
  );
95
95
 
96
96
  if (hasPl && matchedTradeColumns.length >= 2) {
@@ -99,7 +99,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
99
99
  // but also have "Initial Premium" (or aliases) which tradelogs use "Premium" instead.
100
100
  const reportingOnlyColumns = ["initial premium", "initial credit", "initial premium ($)"];
101
101
  const hasReportingOnly = reportingOnlyColumns.some((col) =>
102
- headers.some((h) => h.includes(col))
102
+ headers.some((h) => h.includes(col)),
103
103
  );
104
104
  if (hasReportingOnly) {
105
105
  return "reportinglog";
@@ -110,7 +110,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
110
110
  // Daily log detection:
111
111
  // Required: "Date" (but not "Date Opened"/"Date Closed"), and value column
112
112
  const hasSimpleDate = headers.some(
113
- (h) => h === "date" || (h.includes("date") && !h.includes("opened") && !h.includes("closed"))
113
+ (h) => h === "date" || (h.includes("date") && !h.includes("opened") && !h.includes("closed")),
114
114
  );
115
115
  const valueColumnAliases = [
116
116
  "portfolio value",
@@ -120,7 +120,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
120
120
  "netliquidity",
121
121
  ];
122
122
  const hasValue = valueColumnAliases.some((alias) =>
123
- headers.some((h) => h.includes(alias) || alias.includes(h))
123
+ headers.some((h) => h.includes(alias) || alias.includes(h)),
124
124
  );
125
125
 
126
126
  // Dailylog: has date + value columns but lacks trade-specific columns
@@ -130,26 +130,21 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
130
130
 
131
131
  // TAT (Trade Automation Toolbox) detection:
132
132
  // Has "TradeID" AND "ProfitLoss" AND "BuyingPower"
133
- const tatSignature = ['tradeid', 'profitloss', 'buyingpower'];
133
+ const tatSignature = ["tradeid", "profitloss", "buyingpower"];
134
134
  const isTat = tatSignature.every((sig) => headers.includes(sig));
135
135
  if (isTat) {
136
- return 'reportinglog';
136
+ return "reportinglog";
137
137
  }
138
138
 
139
139
  // Reporting log detection:
140
140
  // Has "Actual P/L" or columns from REPORTING_TRADE_COLUMN_ALIASES
141
- const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map(
142
- (k) => k.toLowerCase()
143
- );
144
- const hasReportingColumns = reportingAliases.some((alias) =>
145
- headers.includes(alias)
146
- );
147
- const hasActualPl = headers.some(
148
- (h) => h.includes("actual") && h.includes("p")
141
+ const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map((k) =>
142
+ k.toLowerCase(),
149
143
  );
144
+ const hasReportingColumns = reportingAliases.some((alias) => headers.includes(alias));
145
+ const hasActualPl = headers.some((h) => h.includes("actual") && h.includes("p"));
150
146
  const hasReportedStyle =
151
- headers.includes("trade id") ||
152
- headers.some((h) => h.includes("reported"));
147
+ headers.includes("trade id") || headers.some((h) => h.includes("reported"));
153
148
 
154
149
  if (hasActualPl || hasReportingColumns || hasReportedStyle) {
155
150
  // Double-check it's not a regular tradelog
@@ -174,7 +169,7 @@ export async function detectCsvType(filePath: string): Promise<CsvType> {
174
169
  * Returns mapping of detected CSV types to filenames.
175
170
  */
176
171
  export async function discoverCsvFiles(
177
- folderPath: string
172
+ folderPath: string,
178
173
  ): Promise<{ mappings: CsvMappings; unrecognized: string[] }> {
179
174
  const mappings: CsvMappings = {};
180
175
  const unrecognized: string[] = [];
@@ -252,11 +247,10 @@ export async function discoverCsvFiles(
252
247
  /**
253
248
  * Log warning when folder has CSVs but none match expected patterns
254
249
  */
255
- export function logCsvDiscoveryWarning(
256
- folderName: string,
257
- csvFiles: string[]
258
- ): void {
259
- console.error(`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`);
250
+ export function logCsvDiscoveryWarning(folderName: string, csvFiles: string[]): void {
251
+ console.error(
252
+ `Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`,
253
+ );
260
254
  console.error(` Found: ${csvFiles.join(", ")}`);
261
255
  console.error(` Expected columns: P/L, Date Opened, Date Closed, Symbol, Strategy`);
262
256
  }
@@ -52,7 +52,7 @@ export interface DataQuality {
52
52
  /** Calendar dates where no data was found at all. */
53
53
  missingDataDates: string[];
54
54
  /** Overall confidence level based on data density and coverage. */
55
- confidenceLevel: 'high' | 'medium' | 'low';
55
+ confidenceLevel: "high" | "medium" | "low";
56
56
  }
57
57
 
58
58
  /**
@@ -106,27 +106,23 @@ export interface CoverageResult {
106
106
  * medium — everything else
107
107
  */
108
108
  export function scoreDataQuality(input: DataQualityInput): DataQuality {
109
- const avgBarsPerDay =
110
- input.tradingDays > 0
111
- ? Math.round(input.totalBars / input.tradingDays)
112
- : 0;
109
+ const avgBarsPerDay = input.tradingDays > 0 ? Math.round(input.totalBars / input.tradingDays) : 0;
113
110
 
114
111
  // Total days = tradingDays (days with data) + daysWithNoData
115
112
  const totalDaysInRange = input.tradingDays + input.daysWithNoData;
116
- const missingPct =
117
- totalDaysInRange > 0 ? input.daysWithNoData / totalDaysInRange : 1;
113
+ const missingPct = totalDaysInRange > 0 ? input.daysWithNoData / totalDaysInRange : 1;
118
114
 
119
- let confidenceLevel: 'high' | 'medium' | 'low';
115
+ let confidenceLevel: "high" | "medium" | "low";
120
116
 
121
117
  if (input.tradingDays === 0 || avgBarsPerDay < 50) {
122
- confidenceLevel = 'low';
123
- } else if (missingPct > 0.10) {
118
+ confidenceLevel = "low";
119
+ } else if (missingPct > 0.1) {
124
120
  // More than 10% of calendar trading days missing → cap at low
125
- confidenceLevel = 'low';
121
+ confidenceLevel = "low";
126
122
  } else if (avgBarsPerDay >= 200 && missingPct <= 0.05) {
127
- confidenceLevel = 'high';
123
+ confidenceLevel = "high";
128
124
  } else {
129
- confidenceLevel = 'medium';
125
+ confidenceLevel = "medium";
130
126
  }
131
127
 
132
128
  return {
@@ -169,12 +165,11 @@ export async function queryCoverage(
169
165
  const quoteCov = await stores.quote.getCoverage(underlying, fromDate, toDate);
170
166
 
171
167
  // Build covered-date set.
172
- const dates = enumerateCoveredDates(spotCov.earliest, spotCov.latest)
173
- .concat(enumerateCoveredDates(quoteCov.earliest, quoteCov.latest));
174
- const uniqueDates = [...new Set(dates)].sort();
175
- const quoteDateSet = new Set(
168
+ const dates = enumerateCoveredDates(spotCov.earliest, spotCov.latest).concat(
176
169
  enumerateCoveredDates(quoteCov.earliest, quoteCov.latest),
177
170
  );
171
+ const uniqueDates = [...new Set(dates)].sort();
172
+ const quoteDateSet = new Set(enumerateCoveredDates(quoteCov.earliest, quoteCov.latest));
178
173
 
179
174
  if (uniqueDates.length === 0) {
180
175
  return {
@@ -230,10 +225,10 @@ function enumerateCoveredDates(from: string | null, to: string | null): string[]
230
225
  // ---------------------------------------------------------------------------
231
226
 
232
227
  /** Classify bar count into density label. */
233
- function densityLabel(barCount: number): 'dense' | 'sparse' | 'none' {
234
- if (barCount >= 200) return 'dense';
235
- if (barCount > 0) return 'sparse';
236
- return 'none';
228
+ function densityLabel(barCount: number): "dense" | "sparse" | "none" {
229
+ if (barCount >= 200) return "dense";
230
+ if (barCount > 0) return "sparse";
231
+ return "none";
237
232
  }
238
233
 
239
234
  /**
@@ -247,10 +242,7 @@ function densityLabel(barCount: number): 'dense' | 'sparse' | 'none' {
247
242
  * 2025-04-01 to 2025-06-30: no data
248
243
  * ```
249
244
  */
250
- export function formatCoverageReport(
251
- tickerPattern: string,
252
- coverage: CoverageResult,
253
- ): string {
245
+ export function formatCoverageReport(tickerPattern: string, coverage: CoverageResult): string {
254
246
  if (coverage.dateBreakdown.length === 0) {
255
247
  return `No data found for ${tickerPattern}`;
256
248
  }
@@ -264,7 +256,7 @@ export function formatCoverageReport(
264
256
  interface Group {
265
257
  fromDate: string;
266
258
  toDate: string;
267
- density: 'dense' | 'sparse' | 'none';
259
+ density: "dense" | "sparse" | "none";
268
260
  barCounts: number[];
269
261
  hasQuotes: boolean;
270
262
  }
@@ -295,27 +287,23 @@ export function formatCoverageReport(
295
287
  for (const group of groups) {
296
288
  const avgBars =
297
289
  group.barCounts.length > 0
298
- ? Math.round(
299
- group.barCounts.reduce((s, v) => s + v, 0) / group.barCounts.length,
300
- )
290
+ ? Math.round(group.barCounts.reduce((s, v) => s + v, 0) / group.barCounts.length)
301
291
  : 0;
302
292
 
303
293
  const rangeStr =
304
- group.fromDate === group.toDate
305
- ? group.fromDate
306
- : `${group.fromDate} to ${group.toDate}`;
294
+ group.fromDate === group.toDate ? group.fromDate : `${group.fromDate} to ${group.toDate}`;
307
295
 
308
296
  let detail: string;
309
- if (group.density === 'dense') {
297
+ if (group.density === "dense") {
310
298
  detail = `dense (avg ${avgBars} bars/day, quotes available)`;
311
- } else if (group.density === 'sparse') {
299
+ } else if (group.density === "sparse") {
312
300
  detail = `sparse (avg ${avgBars} bars/day, trade bars only)`;
313
301
  } else {
314
- detail = 'no data';
302
+ detail = "no data";
315
303
  }
316
304
 
317
305
  lines.push(` ${rangeStr}: ${detail}`);
318
306
  }
319
307
 
320
- return lines.join('\n');
308
+ return lines.join("\n");
321
309
  }
@@ -7,28 +7,28 @@
7
7
  * Provides the computational heart of the `analyze_exit_triggers` tool.
8
8
  */
9
9
 
10
- import type { PnlPoint, ReplayLeg } from './trade-replay.ts';
10
+ import type { PnlPoint, ReplayLeg } from "./trade-replay.ts";
11
11
 
12
12
  // ---------------------------------------------------------------------------
13
13
  // Types
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
16
  export type TriggerType =
17
- | 'profitTarget'
18
- | 'stopLoss'
19
- | 'trailingStop'
20
- | 'profitAction'
21
- | 'dteExit'
22
- | 'ditExit'
23
- | 'clockTimeExit'
24
- | 'underlyingPriceMove'
25
- | 'positionDelta'
26
- | 'perLegDelta'
27
- | 'vixMove'
28
- | 'vix9dMove'
29
- | 'vix9dVixRatio'
30
- | 'slRatioThreshold'
31
- | 'slRatioMove';
17
+ | "profitTarget"
18
+ | "stopLoss"
19
+ | "trailingStop"
20
+ | "profitAction"
21
+ | "dteExit"
22
+ | "ditExit"
23
+ | "clockTimeExit"
24
+ | "underlyingPriceMove"
25
+ | "positionDelta"
26
+ | "perLegDelta"
27
+ | "vixMove"
28
+ | "vix9dMove"
29
+ | "vix9dVixRatio"
30
+ | "slRatioThreshold"
31
+ | "slRatioMove";
32
32
 
33
33
  export interface PartialClose {
34
34
  index: number;
@@ -40,49 +40,49 @@ export interface PartialClose {
40
40
  export interface ExitTriggerConfig {
41
41
  type: TriggerType;
42
42
  threshold: number;
43
- unit?: 'percent' | 'dollar'; // D-07: default 'dollar', backwards compatible
43
+ unit?: "percent" | "dollar"; // D-07: default 'dollar', backwards compatible
44
44
  steps?: Array<{ armAt: number; stopAt: number; closeAllocationPct?: number }>;
45
45
  // Context-specific optional fields:
46
- expiry?: string; // YYYY-MM-DD for dteExit
47
- openDate?: string; // YYYY-MM-DD for ditExit
48
- clockTime?: string; // "HH:MM" for clockTimeExit (threshold ignored)
49
- trailAmount?: number; // Dollar trail for trailingStop
46
+ expiry?: string; // YYYY-MM-DD for dteExit
47
+ openDate?: string; // YYYY-MM-DD for ditExit
48
+ clockTime?: string; // "HH:MM" for clockTimeExit (threshold ignored)
49
+ trailAmount?: number; // Dollar trail for trailingStop
50
50
  // Directional delta fields (per-leg directional exits):
51
- legIndex?: number; // 0-based leg index for perLegDelta — targets specific leg
52
- exitAbove?: number; // Fire when value > exitAbove (directional, no abs)
53
- exitBelow?: number; // Fire when value < exitBelow (directional, no abs)
51
+ legIndex?: number; // 0-based leg index for perLegDelta — targets specific leg
52
+ exitAbove?: number; // Fire when value > exitAbove (directional, no abs)
53
+ exitBelow?: number; // Fire when value < exitBelow (directional, no abs)
54
54
  // Data maps for triggers needing external prices:
55
- underlyingPrices?: Map<string, number>; // timestamp -> price
56
- vixPrices?: Map<string, number>; // timestamp -> VIX price
57
- vix9dPrices?: Map<string, number>; // timestamp -> VIX9D price
55
+ underlyingPrices?: Map<string, number>; // timestamp -> price
56
+ vixPrices?: Map<string, number>; // timestamp -> VIX price
57
+ vix9dPrices?: Map<string, number>; // timestamp -> VIX9D price
58
58
  // S/L ratio inputs:
59
- spreadWidth?: number; // Width of spread in dollars
60
- contracts?: number; // Number of contracts
61
- multiplier?: number; // Default 100
62
- // profitTarget confirmation: N synchronized-quote bars at-or-above threshold required before firing (default 1 = fire on first cross)
59
+ spreadWidth?: number; // Width of spread in dollars
60
+ contracts?: number; // Number of contracts
61
+ multiplier?: number; // Default 100
62
+ // profitTarget confirmation: N synchronized-quote bars at-or-above threshold required before firing (default 2 = require two synchronized-quote confirmations before firing; set to 1 to fire on the first cross)
63
63
  requiredHits?: number;
64
64
  // Internal: set by handler when unit='percent' to compute dollar threshold
65
- entryCost?: number; // D-11: cost/credit of entry (negative = credit received)
66
- entrySLRatio?: number; // Runtime-hydrated opening short/long ratio for slRatioMove
65
+ entryCost?: number; // D-11: cost/credit of entry (negative = credit received)
66
+ entrySLRatio?: number; // Runtime-hydrated opening short/long ratio for slRatioMove
67
67
  }
68
68
 
69
69
  export interface TriggerFireEvent {
70
70
  type: TriggerType;
71
- firedAt: string; // Timestamp when trigger fired
72
- pnlAtFire: number; // Strategy P&L when trigger fired
73
- index: number; // Index into pnlPath
74
- detail?: string; // Human-readable description
71
+ firedAt: string; // Timestamp when trigger fired
72
+ pnlAtFire: number; // Strategy P&L when trigger fired
73
+ index: number; // Index into pnlPath
74
+ detail?: string; // Human-readable description
75
75
  }
76
76
 
77
77
  export interface ExitTriggerResult {
78
- triggers: TriggerFireEvent[]; // All triggers that fired (sorted by fire time)
78
+ triggers: TriggerFireEvent[]; // All triggers that fired (sorted by fire time)
79
79
  firstToFire: TriggerFireEvent | null; // Earliest trigger
80
80
  actualExit?: {
81
81
  timestamp: string;
82
82
  pnl: number;
83
- pnlDifference: number; // firstToFire.pnl - actualExit.pnl
83
+ pnlDifference: number; // firstToFire.pnl - actualExit.pnl
84
84
  };
85
- partialCloses?: PartialClose[]; // Partial position closes from profitAction steps
85
+ partialCloses?: PartialClose[]; // Partial position closes from profitAction steps
86
86
  summary: string;
87
87
  }
88
88
 
@@ -104,7 +104,7 @@ export interface LegGroupResult {
104
104
 
105
105
  /** Parse "YYYY-MM-DD" to a Date at local midnight. */
106
106
  function parseDate(dateStr: string): Date {
107
- const [y, m, d] = dateStr.split('-').map(Number);
107
+ const [y, m, d] = dateStr.split("-").map(Number);
108
108
  return new Date(y, m - 1, d);
109
109
  }
110
110
 
@@ -181,13 +181,11 @@ export function evaluateProfitAction(
181
181
  if (pnlPath.length === 0 || !trigger.steps?.length) {
182
182
  return { fireEvent: null, partialCloses };
183
183
  }
184
- if (trigger.unit === 'percent' && trigger.entryCost == null) {
184
+ if (trigger.unit === "percent" && trigger.entryCost == null) {
185
185
  return { fireEvent: null, partialCloses };
186
186
  }
187
187
 
188
- const scale = trigger.unit === 'percent'
189
- ? Math.abs(trigger.entryCost!)
190
- : 1;
188
+ const scale = trigger.unit === "percent" ? Math.abs(trigger.entryCost!) : 1;
191
189
 
192
190
  const normalizedSteps = [...trigger.steps]
193
191
  .sort((a, b) => a.armAt - b.armAt)
@@ -218,7 +216,7 @@ export function evaluateProfitAction(
218
216
  index: i,
219
217
  pnlAtFire: pnl * remainingAllocation * step.closeAllocationPct,
220
218
  allocation: closeAmt,
221
- trigger: 'profitAction',
219
+ trigger: "profitAction",
222
220
  });
223
221
  remainingAllocation -= closeAmt;
224
222
  }
@@ -237,13 +235,14 @@ export function evaluateProfitAction(
237
235
  // Simplifies to: pnl <= activeFloor (when remainingAllocation > 0)
238
236
  if (activeFloor > -Infinity && remainingAllocation > 0 && pnl <= activeFloor) {
239
237
  const effectivePnl = pnl * remainingAllocation;
240
- const detail = trigger.unit === 'percent'
241
- ? `Profit action: stop adjusted to ${(activeFloor / scale * 100).toFixed(0)}% ($${activeFloor.toFixed(2)}) at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`
242
- : `Profit action: stop adjusted to $${activeFloor.toFixed(2)} at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`;
238
+ const detail =
239
+ trigger.unit === "percent"
240
+ ? `Profit action: stop adjusted to ${((activeFloor / scale) * 100).toFixed(0)}% ($${activeFloor.toFixed(2)}) at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`
241
+ : `Profit action: stop adjusted to $${activeFloor.toFixed(2)} at max P&L $${runningMaxPnl.toFixed(2)}, hit at $${pnl.toFixed(2)} (remaining ${(remainingAllocation * 100).toFixed(0)}%)`;
243
242
 
244
243
  return {
245
244
  fireEvent: {
246
- type: 'profitAction',
245
+ type: "profitAction",
247
246
  firedAt: point.timestamp,
248
247
  pnlAtFire: effectivePnl,
249
248
  index: i,
@@ -293,44 +292,44 @@ export function evaluateTrigger(
293
292
  let detail: string | undefined;
294
293
 
295
294
  switch (type) {
296
- case 'profitTarget': {
295
+ case "profitTarget": {
297
296
  // unit='percent' requires entryCost; if missing, cannot compute — no fire
298
- if (trigger.unit === 'percent' && trigger.entryCost == null) break;
299
- const requiredHits = trigger.requiredHits ?? 1;
300
- const dollarThresholdPT = trigger.unit === 'percent'
301
- ? threshold * Math.abs(trigger.entryCost!)
302
- : threshold;
297
+ if (trigger.unit === "percent" && trigger.entryCost == null) break;
298
+ const requiredHits = trigger.requiredHits ?? 2;
299
+ const dollarThresholdPT =
300
+ trigger.unit === "percent" ? threshold * Math.abs(trigger.entryCost!) : threshold;
303
301
  if (pnl >= dollarThresholdPT) {
304
302
  if (point.allLegsSync !== false) profitTargetHits++;
305
303
  if (profitTargetHits < requiredHits) break;
306
304
  fired = true;
307
- detail = trigger.unit === 'percent'
308
- ? `P&L $${pnl.toFixed(2)} >= ${(threshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} ($${dollarThresholdPT.toFixed(2)})`
309
- : `P&L $${pnl.toFixed(2)} >= target $${dollarThresholdPT.toFixed(2)}`;
305
+ detail =
306
+ trigger.unit === "percent"
307
+ ? `P&L $${pnl.toFixed(2)} >= ${(threshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} ($${dollarThresholdPT.toFixed(2)})`
308
+ : `P&L $${pnl.toFixed(2)} >= target $${dollarThresholdPT.toFixed(2)}`;
310
309
  } else if (point.allLegsSync !== false) {
311
310
  profitTargetHits = 0;
312
311
  }
313
312
  break;
314
313
  }
315
314
 
316
- case 'stopLoss': {
315
+ case "stopLoss": {
317
316
  // Normalize negative threshold — users may pass -2 meaning "stop at $2 loss"
318
317
  const absThreshold = Math.abs(threshold);
319
318
  // unit='percent' requires entryCost; if missing, cannot compute — no fire
320
- if (trigger.unit === 'percent' && trigger.entryCost == null) break;
321
- const dollarThresholdSL = trigger.unit === 'percent'
322
- ? absThreshold * Math.abs(trigger.entryCost!)
323
- : absThreshold;
319
+ if (trigger.unit === "percent" && trigger.entryCost == null) break;
320
+ const dollarThresholdSL =
321
+ trigger.unit === "percent" ? absThreshold * Math.abs(trigger.entryCost!) : absThreshold;
324
322
  if (pnl <= -dollarThresholdSL) {
325
323
  fired = true;
326
- detail = trigger.unit === 'percent'
327
- ? `P&L $${pnl.toFixed(2)} <= -${(absThreshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} (-$${dollarThresholdSL.toFixed(2)})`
328
- : `P&L $${pnl.toFixed(2)} <= stop -$${dollarThresholdSL.toFixed(2)}`;
324
+ detail =
325
+ trigger.unit === "percent"
326
+ ? `P&L $${pnl.toFixed(2)} <= -${(absThreshold * 100).toFixed(0)}% of $${Math.abs(trigger.entryCost!).toFixed(2)} (-$${dollarThresholdSL.toFixed(2)})`
327
+ : `P&L $${pnl.toFixed(2)} <= stop -$${dollarThresholdSL.toFixed(2)}`;
329
328
  }
330
329
  break;
331
330
  }
332
331
 
333
- case 'trailingStop': {
332
+ case "trailingStop": {
334
333
  const trailAmt = trigger.trailAmount ?? threshold;
335
334
  const dropdown = runningMaxPnl - pnl;
336
335
  if (dropdown >= trailAmt && runningMaxPnl > -Infinity) {
@@ -340,7 +339,7 @@ export function evaluateTrigger(
340
339
  break;
341
340
  }
342
341
 
343
- case 'profitAction': {
342
+ case "profitAction": {
344
343
  // Delegate to evaluateProfitAction for the full path evaluation
345
344
  // (evaluateTrigger is called point-by-point in the loop, but profitAction
346
345
  // needs full-path context for partial close tracking, so we handle it
@@ -349,7 +348,7 @@ export function evaluateTrigger(
349
348
  return paResult.fireEvent;
350
349
  }
351
350
 
352
- case 'dteExit': {
351
+ case "dteExit": {
353
352
  if (!trigger.expiry) break;
354
353
  const pointDate = parseDate(extractDate(point.timestamp));
355
354
  const expiryDate = parseDate(trigger.expiry);
@@ -362,7 +361,7 @@ export function evaluateTrigger(
362
361
  break;
363
362
  }
364
363
 
365
- case 'ditExit': {
364
+ case "ditExit": {
366
365
  if (!trigger.openDate) break;
367
366
  const pointDate = parseDate(extractDate(point.timestamp));
368
367
  const openDate = parseDate(trigger.openDate);
@@ -374,8 +373,8 @@ export function evaluateTrigger(
374
373
  break;
375
374
  }
376
375
 
377
- case 'clockTimeExit': {
378
- const clockTime = trigger.clockTime ?? '15:00';
376
+ case "clockTimeExit": {
377
+ const clockTime = trigger.clockTime ?? "15:00";
379
378
  const pointTime = extractTime(point.timestamp);
380
379
  if (pointTime >= clockTime) {
381
380
  fired = true;
@@ -384,7 +383,7 @@ export function evaluateTrigger(
384
383
  break;
385
384
  }
386
385
 
387
- case 'underlyingPriceMove': {
386
+ case "underlyingPriceMove": {
388
387
  if (!trigger.underlyingPrices) break;
389
388
  const price = trigger.underlyingPrices.get(point.timestamp);
390
389
  if (price == null) break;
@@ -400,7 +399,7 @@ export function evaluateTrigger(
400
399
  break;
401
400
  }
402
401
 
403
- case 'positionDelta': {
402
+ case "positionDelta": {
404
403
  const netDelta = point.netDelta ?? 0;
405
404
  if (trigger.exitAbove != null) {
406
405
  if (netDelta > trigger.exitAbove) {
@@ -419,7 +418,7 @@ export function evaluateTrigger(
419
418
  break;
420
419
  }
421
420
 
422
- case 'perLegDelta': {
421
+ case "perLegDelta": {
423
422
  if (!point.legGreeks) break;
424
423
  if (trigger.legIndex != null) {
425
424
  // Target a specific leg
@@ -458,7 +457,7 @@ export function evaluateTrigger(
458
457
  break;
459
458
  }
460
459
 
461
- case 'vixMove': {
460
+ case "vixMove": {
462
461
  if (!trigger.vixPrices) break;
463
462
  const vix = trigger.vixPrices.get(point.timestamp);
464
463
  if (vix == null) break;
@@ -474,7 +473,7 @@ export function evaluateTrigger(
474
473
  break;
475
474
  }
476
475
 
477
- case 'vix9dMove': {
476
+ case "vix9dMove": {
478
477
  if (!trigger.vix9dPrices) break;
479
478
  const vix9d = trigger.vix9dPrices.get(point.timestamp);
480
479
  if (vix9d == null) break;
@@ -490,7 +489,7 @@ export function evaluateTrigger(
490
489
  break;
491
490
  }
492
491
 
493
- case 'vix9dVixRatio': {
492
+ case "vix9dVixRatio": {
494
493
  if (!trigger.vixPrices || !trigger.vix9dPrices) break;
495
494
  const vix = trigger.vixPrices.get(point.timestamp);
496
495
  const vix9d = trigger.vix9dPrices.get(point.timestamp);
@@ -506,7 +505,7 @@ export function evaluateTrigger(
506
505
  break;
507
506
  }
508
507
 
509
- case 'slRatioThreshold': {
508
+ case "slRatioThreshold": {
510
509
  const sw = trigger.spreadWidth ?? 0;
511
510
  const ct = trigger.contracts ?? 1;
512
511
  const mp = trigger.multiplier ?? 100;
@@ -519,13 +518,13 @@ export function evaluateTrigger(
519
518
  break;
520
519
  }
521
520
 
522
- case 'slRatioMove': {
521
+ case "slRatioMove": {
523
522
  const sw = trigger.spreadWidth ?? 0;
524
523
  const ct = trigger.contracts ?? 1;
525
524
  const mp = trigger.multiplier ?? 100;
526
525
  if (sw === 0) break;
527
526
  const slRatio = computeSLRatio(point, legs, sw, ct, mp);
528
- if (initialSLRatio === null && typeof trigger.entrySLRatio === 'number') {
527
+ if (initialSLRatio === null && typeof trigger.entrySLRatio === "number") {
529
528
  initialSLRatio = trigger.entrySLRatio;
530
529
  }
531
530
  if (initialSLRatio === null) {
@@ -563,11 +562,7 @@ export function evaluateTrigger(
563
562
  * Compute per-group P&L at each timestamp from leg prices.
564
563
  * groupPnl[t] = sum over legIndices of (legPrices[i] - entryPrice[i]) * quantity[i] * multiplier[i]
565
564
  */
566
- function computeGroupPnl(
567
- pnlPath: PnlPoint[],
568
- legs: ReplayLeg[],
569
- legIndices: number[],
570
- ): number[] {
565
+ function computeGroupPnl(pnlPath: PnlPoint[], legs: ReplayLeg[], legIndices: number[]): number[] {
571
566
  return pnlPath.map((point) => {
572
567
  let groupPnl = 0;
573
568
  for (const idx of legIndices) {
@@ -605,7 +600,7 @@ export function analyzeExitTriggers(config: {
605
600
  const fireEvents: TriggerFireEvent[] = [];
606
601
  let allPartialCloses: PartialClose[] = [];
607
602
  for (const trigger of triggers) {
608
- if (trigger.type === 'profitAction') {
603
+ if (trigger.type === "profitAction") {
609
604
  // Use the partial-close-aware helper for profitAction
610
605
  const paResult = evaluateProfitAction(trigger, pnlPath, legs);
611
606
  if (paResult.fireEvent) {
@@ -628,7 +623,7 @@ export function analyzeExitTriggers(config: {
628
623
  const firstToFire = fireEvents.length > 0 ? fireEvents[0] : null;
629
624
 
630
625
  // Actual exit comparison
631
- let actualExit: ExitTriggerResult['actualExit'];
626
+ let actualExit: ExitTriggerResult["actualExit"];
632
627
  if (actualExitTimestamp && firstToFire) {
633
628
  // Find closest point to actualExitTimestamp
634
629
  let closestIdx = 0;
@@ -662,12 +657,14 @@ export function analyzeExitTriggers(config: {
662
657
  if (!firstToFire) {
663
658
  summary = `No triggers fired across ${pnlPath.length} data points.`;
664
659
  } else if (actualExit) {
665
- const betterWorse = actualExit.pnlDifference > 0 ? 'better' : 'worse';
666
- summary = `${firstToFire.type} fired at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
660
+ const betterWorse = actualExit.pnlDifference > 0 ? "better" : "worse";
661
+ summary =
662
+ `${firstToFire.type} fired at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
667
663
  `Actual exit at ${actualExit.timestamp} (P&L $${actualExit.pnl.toFixed(2)}). ` +
668
664
  `Trigger was $${Math.abs(actualExit.pnlDifference).toFixed(2)} ${betterWorse}.`;
669
665
  } else {
670
- summary = `${firstToFire.type} fired first at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
666
+ summary =
667
+ `${firstToFire.type} fired first at ${firstToFire.firedAt} (P&L $${firstToFire.pnlAtFire.toFixed(2)}). ` +
671
668
  `${fireEvents.length} trigger(s) fired total.`;
672
669
  }
673
670
 
@@ -691,9 +688,7 @@ export function analyzeExitTriggers(config: {
691
688
  strategyPnl: groupPnlArr[idx],
692
689
  // Filter legPrices/legGreeks to only this group's legs
693
690
  legPrices: group.legIndices.map((li) => point.legPrices[li] ?? 0),
694
- legGreeks: point.legGreeks
695
- ? group.legIndices.map((li) => point.legGreeks![li])
696
- : undefined,
691
+ legGreeks: point.legGreeks ? group.legIndices.map((li) => point.legGreeks![li]) : undefined,
697
692
  }));
698
693
 
699
694
  // Build group legs subset
@@ -710,7 +705,7 @@ export function analyzeExitTriggers(config: {
710
705
  const groupFirstToFire = groupFireEvents.length > 0 ? groupFireEvents[0] : null;
711
706
 
712
707
  // Actual exit for group
713
- let groupActualExit: ExitTriggerResult['actualExit'];
708
+ let groupActualExit: ExitTriggerResult["actualExit"];
714
709
  if (actualExitTimestamp && groupFirstToFire) {
715
710
  let closestIdx = pnlPath.length - 1;
716
711
  for (let i = 0; i < pnlPath.length; i++) {