@matchstick-trading/primitives 0.0.10

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 (216) hide show
  1. package/AGENTS.md +44 -0
  2. package/CHANGELOG.md +19 -0
  3. package/LICENSE +201 -0
  4. package/README.md +636 -0
  5. package/dist/Chart.d.ts +18 -0
  6. package/dist/Chart.js +6 -0
  7. package/dist/ChartParts.d.ts +151 -0
  8. package/dist/ChartParts.js +389 -0
  9. package/dist/CsvDrop.d.ts +8 -0
  10. package/dist/CsvDrop.js +5 -0
  11. package/dist/CsvDropParts.d.ts +33 -0
  12. package/dist/CsvDropParts.js +77 -0
  13. package/dist/Cursor.d.ts +6 -0
  14. package/dist/Cursor.js +5 -0
  15. package/dist/CursorParts.d.ts +51 -0
  16. package/dist/CursorParts.js +40 -0
  17. package/dist/DepthProfile.d.ts +13 -0
  18. package/dist/DepthProfile.js +5 -0
  19. package/dist/DepthProfileParts.d.ts +45 -0
  20. package/dist/DepthProfileParts.js +96 -0
  21. package/dist/DiffTable.d.ts +13 -0
  22. package/dist/DiffTable.js +5 -0
  23. package/dist/DiffTableParts.d.ts +57 -0
  24. package/dist/DiffTableParts.js +103 -0
  25. package/dist/Drawings.d.ts +13 -0
  26. package/dist/Drawings.js +5 -0
  27. package/dist/DrawingsParts.d.ts +194 -0
  28. package/dist/DrawingsParts.js +775 -0
  29. package/dist/Fullscreen.d.ts +7 -0
  30. package/dist/Fullscreen.js +5 -0
  31. package/dist/FullscreenParts.d.ts +54 -0
  32. package/dist/FullscreenParts.js +250 -0
  33. package/dist/Ladder.d.ts +13 -0
  34. package/dist/Ladder.js +5 -0
  35. package/dist/LadderParts.d.ts +49 -0
  36. package/dist/LadderParts.js +56 -0
  37. package/dist/Menu.d.ts +11 -0
  38. package/dist/Menu.js +5 -0
  39. package/dist/MenuParts.d.ts +52 -0
  40. package/dist/MenuParts.js +202 -0
  41. package/dist/Notice.d.ts +18 -0
  42. package/dist/Notice.js +9 -0
  43. package/dist/Panels.d.ts +9 -0
  44. package/dist/Panels.js +5 -0
  45. package/dist/PanelsParts.d.ts +62 -0
  46. package/dist/PanelsParts.js +387 -0
  47. package/dist/Receipt.d.ts +11 -0
  48. package/dist/Receipt.js +5 -0
  49. package/dist/ReceiptParts.d.ts +45 -0
  50. package/dist/ReceiptParts.js +71 -0
  51. package/dist/Scrubber.d.ts +9 -0
  52. package/dist/Scrubber.js +5 -0
  53. package/dist/ScrubberParts.d.ts +74 -0
  54. package/dist/ScrubberParts.js +126 -0
  55. package/dist/Selection.d.ts +14 -0
  56. package/dist/Selection.js +7 -0
  57. package/dist/SelectionParts.d.ts +63 -0
  58. package/dist/SelectionParts.js +208 -0
  59. package/dist/Studies.d.ts +9 -0
  60. package/dist/Studies.js +5 -0
  61. package/dist/StudiesParts.d.ts +45 -0
  62. package/dist/StudiesParts.js +153 -0
  63. package/dist/Submenu.d.ts +7 -0
  64. package/dist/Submenu.js +4 -0
  65. package/dist/SymbolSearch.d.ts +10 -0
  66. package/dist/SymbolSearch.js +5 -0
  67. package/dist/SymbolSearchParts.d.ts +39 -0
  68. package/dist/SymbolSearchParts.js +86 -0
  69. package/dist/Tape.d.ts +15 -0
  70. package/dist/Tape.js +5 -0
  71. package/dist/TapeParts.d.ts +69 -0
  72. package/dist/TapeParts.js +139 -0
  73. package/dist/Toolbar.d.ts +8 -0
  74. package/dist/Toolbar.js +5 -0
  75. package/dist/ToolbarParts.d.ts +61 -0
  76. package/dist/ToolbarParts.js +122 -0
  77. package/dist/Transport.d.ts +15 -0
  78. package/dist/Transport.js +5 -0
  79. package/dist/TransportParts.d.ts +52 -0
  80. package/dist/TransportParts.js +47 -0
  81. package/dist/VolumeProfile.d.ts +7 -0
  82. package/dist/VolumeProfile.js +5 -0
  83. package/dist/VolumeProfileParts.d.ts +37 -0
  84. package/dist/VolumeProfileParts.js +57 -0
  85. package/dist/accessible-name.d.ts +26 -0
  86. package/dist/accessible-name.js +14 -0
  87. package/dist/attributes.d.ts +8 -0
  88. package/dist/attributes.js +9 -0
  89. package/dist/core/adapter.d.ts +38 -0
  90. package/dist/core/adapter.js +64 -0
  91. package/dist/core/attributes.d.ts +36 -0
  92. package/dist/core/attributes.js +22 -0
  93. package/dist/core/candles.d.ts +6 -0
  94. package/dist/core/candles.js +19 -0
  95. package/dist/core/chart-gesture.d.ts +109 -0
  96. package/dist/core/chart-gesture.js +161 -0
  97. package/dist/core/chart-scale.d.ts +141 -0
  98. package/dist/core/chart-scale.js +302 -0
  99. package/dist/core/csv-bars.d.ts +54 -0
  100. package/dist/core/csv-bars.js +325 -0
  101. package/dist/core/depth-profile.d.ts +43 -0
  102. package/dist/core/depth-profile.js +66 -0
  103. package/dist/core/diff-table.d.ts +57 -0
  104. package/dist/core/diff-table.js +77 -0
  105. package/dist/core/drawing-fields.d.ts +11 -0
  106. package/dist/core/drawing-fields.js +15 -0
  107. package/dist/core/drawings.d.ts +194 -0
  108. package/dist/core/drawings.js +403 -0
  109. package/dist/core/fullscreen.d.ts +32 -0
  110. package/dist/core/fullscreen.js +16 -0
  111. package/dist/core/index.d.ts +41 -0
  112. package/dist/core/index.js +23 -0
  113. package/dist/core/ladder-keys.d.ts +35 -0
  114. package/dist/core/ladder-keys.js +43 -0
  115. package/dist/core/ladder.d.ts +65 -0
  116. package/dist/core/ladder.js +93 -0
  117. package/dist/core/panels.d.ts +72 -0
  118. package/dist/core/panels.js +151 -0
  119. package/dist/core/receipt-rules.d.ts +42 -0
  120. package/dist/core/receipt-rules.js +20 -0
  121. package/dist/core/scrubber-gesture.d.ts +66 -0
  122. package/dist/core/scrubber-gesture.js +69 -0
  123. package/dist/core/selection.d.ts +24 -0
  124. package/dist/core/selection.js +33 -0
  125. package/dist/core/series.d.ts +161 -0
  126. package/dist/core/series.js +331 -0
  127. package/dist/core/studies.d.ts +80 -0
  128. package/dist/core/studies.js +237 -0
  129. package/dist/core/symbol-search.d.ts +89 -0
  130. package/dist/core/symbol-search.js +223 -0
  131. package/dist/core/tape.d.ts +77 -0
  132. package/dist/core/tape.js +136 -0
  133. package/dist/core/volume-profile.d.ts +47 -0
  134. package/dist/core/volume-profile.js +86 -0
  135. package/dist/index.d.ts +82 -0
  136. package/dist/index.js +42 -0
  137. package/dist/ladder-keys.d.ts +16 -0
  138. package/dist/ladder-keys.js +86 -0
  139. package/dist/panels-context.d.ts +47 -0
  140. package/dist/panels-context.js +3 -0
  141. package/dist/selection-context.d.ts +41 -0
  142. package/dist/selection-context.js +4 -0
  143. package/examples/csv-to-chart-frame.mjs +17 -0
  144. package/examples/studies.mjs +12 -0
  145. package/llms.txt +16 -0
  146. package/package.json +67 -0
  147. package/src/Chart.tsx +7 -0
  148. package/src/ChartParts.tsx +529 -0
  149. package/src/CsvDrop.tsx +6 -0
  150. package/src/CsvDropParts.tsx +128 -0
  151. package/src/Cursor.tsx +6 -0
  152. package/src/CursorParts.tsx +77 -0
  153. package/src/DepthProfile.tsx +6 -0
  154. package/src/DepthProfileParts.tsx +154 -0
  155. package/src/DiffTable.tsx +6 -0
  156. package/src/DiffTableParts.tsx +160 -0
  157. package/src/Drawings.tsx +6 -0
  158. package/src/DrawingsParts.tsx +1080 -0
  159. package/src/Fullscreen.tsx +6 -0
  160. package/src/FullscreenParts.tsx +318 -0
  161. package/src/Ladder.tsx +6 -0
  162. package/src/LadderParts.tsx +114 -0
  163. package/src/Menu.tsx +6 -0
  164. package/src/MenuParts.tsx +364 -0
  165. package/src/Notice.tsx +27 -0
  166. package/src/Panels.tsx +6 -0
  167. package/src/PanelsParts.tsx +454 -0
  168. package/src/Receipt.tsx +6 -0
  169. package/src/ReceiptParts.tsx +118 -0
  170. package/src/Scrubber.tsx +6 -0
  171. package/src/ScrubberParts.tsx +207 -0
  172. package/src/Selection.tsx +12 -0
  173. package/src/SelectionParts.tsx +251 -0
  174. package/src/Studies.tsx +6 -0
  175. package/src/StudiesParts.tsx +226 -0
  176. package/src/Submenu.tsx +5 -0
  177. package/src/SymbolSearch.tsx +6 -0
  178. package/src/SymbolSearchParts.tsx +166 -0
  179. package/src/Tape.tsx +6 -0
  180. package/src/TapeParts.tsx +208 -0
  181. package/src/Toolbar.tsx +6 -0
  182. package/src/ToolbarParts.tsx +179 -0
  183. package/src/Transport.tsx +6 -0
  184. package/src/TransportParts.tsx +128 -0
  185. package/src/VolumeProfile.tsx +6 -0
  186. package/src/VolumeProfileParts.tsx +106 -0
  187. package/src/accessible-name.tsx +34 -0
  188. package/src/attributes.ts +12 -0
  189. package/src/core/adapter.ts +91 -0
  190. package/src/core/attributes.ts +51 -0
  191. package/src/core/candles.ts +22 -0
  192. package/src/core/chart-gesture.ts +228 -0
  193. package/src/core/chart-scale.ts +368 -0
  194. package/src/core/csv-bars.ts +328 -0
  195. package/src/core/depth-profile.ts +84 -0
  196. package/src/core/diff-table.ts +105 -0
  197. package/src/core/drawing-fields.ts +17 -0
  198. package/src/core/drawings.ts +478 -0
  199. package/src/core/fullscreen.ts +32 -0
  200. package/src/core/index.ts +42 -0
  201. package/src/core/ladder-keys.ts +61 -0
  202. package/src/core/ladder.ts +156 -0
  203. package/src/core/panels.ts +177 -0
  204. package/src/core/receipt-rules.ts +45 -0
  205. package/src/core/scrubber-gesture.ts +102 -0
  206. package/src/core/selection.ts +41 -0
  207. package/src/core/series.ts +374 -0
  208. package/src/core/studies.ts +260 -0
  209. package/src/core/symbol-search.ts +256 -0
  210. package/src/core/tape.ts +159 -0
  211. package/src/core/volume-profile.ts +116 -0
  212. package/src/index.ts +83 -0
  213. package/src/ladder-keys.ts +83 -0
  214. package/src/panels-context.ts +46 -0
  215. package/src/selection-context.ts +38 -0
  216. package/src/tokens.css +235 -0
@@ -0,0 +1,41 @@
1
+ export { numericWidthValue, primitiveAttributes } from './attributes.js';
2
+ export type { Change, CursorMark, DiffStatus, PrimitiveAttributes, PrimitiveState, Quality, Side } from './attributes.js';
3
+ export { isRowShown, LADDER_ROW_ATTRIBUTE, nextLadderRow, resolveLadderKey } from './ladder-keys.js';
4
+ export type { LadderKeyAction, LadderKeyInput } from './ladder-keys.js';
5
+ export { clampFraction, fractionFromPointer, isActiveScrubPointer, isPrimaryScrubPointer, reduceScrubGesture, resolveScrubKey } from './scrubber-gesture.js';
6
+ export type { ScrubGestureEvent, ScrubGestureResult, ScrubKeyAction, ScrubPointer } from './scrubber-gesture.js';
7
+ export { DEPTH_HEIGHT, DEPTH_WIDTH, depthGeometry, depthLevelId, nextDepthIndex, resolveDepthKey } from './depth-profile.js';
8
+ export type { DepthGeometry, DepthKeyAction, DepthLevel } from './depth-profile.js';
9
+ export { DIFF_CHANGE_LABEL, compareExact, diffRowId, holdDiffOrder, nextDiffIndex, nextDiffSort, resolveDiffKey, signedDelta, sortDiffRows } from './diff-table.js';
10
+ export type { DiffChange, DiffKeyAction, DiffMove, DiffSort, DiffTableRow } from './diff-table.js';
11
+ export { TAPE_SIDE_LABEL, holdTape, newestFirst, nextTapeIndex, readTape, resolveTapeKey, tapeSummary, tapeTime } from './tape.js';
12
+ export type { TapeIssue, TapeKeyAction, TapeMove, TapeRead, TapeRow, TapeSide, TapeTrade } from './tape.js';
13
+ export { buildLadder } from './ladder.js';
14
+ export type { LadderBuilt, LadderInput, LadderIssue, LadderLevelIn, LadderRow, LadderSide } from './ladder.js';
15
+ export { receiptHash, receiptVerdict, resolveReceiptKey } from './receipt-rules.js';
16
+ export type { ReceiptData, ReceiptHashOf, ReceiptVerdict } from './receipt-rules.js';
17
+ export { buildSymbolIndex, nextSymbolIndex, normalizeSymbolText, resolveSymbolKey, searchSymbols } from './symbol-search.js';
18
+ export type { Instrument, MatchHow, MatchedOn, SearchOptions, SymbolIndex, SymbolKeyAction, SymbolMatch } from './symbol-search.js';
19
+ export { barInterval, chartFrame, clampPriceRange, dataPriceSpan, growViewport, indexOfTime, lastBars, panPriceRange, panViewport, priceTicks, resolveChartKey, tickDecimals, timeOfIndex, zoomPriceRange, zoomViewport } from './chart-scale.js';
20
+ export type { CandleBar, ChartFrame, ChartFrameOptions, ChartInclude, PriceSpan, ChartKeyAction, ChartSize, ChartViewport } from './chart-scale.js';
21
+ export { INITIAL_CHART_GESTURE, isChartGestureActive, reduceChartGesture } from './chart-gesture.js';
22
+ export type { ChartGestureEvent, ChartGestureResult, ChartGestureState, ChartGestureView, ChartPoint } from './chart-gesture.js';
23
+ export { atr, bollinger, ema, rsi, sma, sourcePrice, studyRange, visibleIndexes, vwap, wma } from './studies.js';
24
+ export type { BollingerBands, Series, StudySource, VwapBand, VwapOptions, VwapResult } from './studies.js';
25
+ export { indexRangeOfTime, volumeProfile } from './volume-profile.js';
26
+ export type { ProfileRow, VolumeProfileOptions, VolumeProfileResult } from './volume-profile.js';
27
+ export { ATTACH_LABELS, ATTACH_MODES, DRAWING_TOOLS, DRAWING_TOOL_LABELS, FIB_RATIOS, anchorCount, attachOf, barTime, barsFromFirst, barsFromLast, timeOfBarFromLast, describeAnchor, describeDrawing, drawingGeometry, drawingPriceRanges, editDrawing, hitTest, moveAnchor, moveDrawing, nextDrawingId, nudgeAmount, resolveDrawingKey, snapToBar } from './drawings.js';
28
+ export type { AttachMode, Drawing, DrawingAnchor, DrawingGeometry, DrawingHit, DrawingKeyAction, DrawingTool, SnapMode } from './drawings.js';
29
+ export { describeBarsCsv, parseBarsCsv } from './csv-bars.js';
30
+ export type { CsvBarsError, CsvBarsNote, CsvBarsOptions, CsvBarsResult, CsvExtraColumn } from './csv-bars.js';
31
+ export { formatBarTime, heikinAshi } from './candles.js';
32
+ export { barField, barNote, fromPriceField, fromTimeField, parseBarField, toTimeField } from './drawing-fields.js';
33
+ export { describeSelection, sameSelection, selectionAfterClearTap, selectionAfterEscape, selectionAfterTap } from './selection.js';
34
+ export type { ChartSelection, SelectionNames } from './selection.js';
35
+ export { AXIS_BOTTOM, MAIN_PANEL, describeMove, moveSeries, moveTargets, normalizeLayout, panelBands, panelOf, reconcileLayout } from './panels.js';
36
+ export type { MoveDestination, MoveTarget, PanelBand, PanelLayout, PanelSeriesRequest, PanelSpec } from './panels.js';
37
+ export { POINT_KINDS, alignSeries, duplicateIds, pointsFromValues, report, resolveSeries, seriesSetSchema, validateSeriesSet } from './series.js';
38
+ export type { AlignMode, AlignOptions, AlignedSeries, AlignedState, DataSeries, FindingKind, PointKind, ResolvedSeries, SeriesCalendar, SeriesFinding, SeriesInput, SeriesIssue, SeriesPoint, SeriesSet, SeriesValidation } from './series.js';
39
+ export { csvAdapter, toSeriesSet } from './adapter.js';
40
+ export type { AdapterIssue, AdapterResult, CsvAdapterOptions, DataAdapter } from './adapter.js';
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,23 @@
1
+ // The plain-TypeScript core of the primitives: geometry, state rules, studies and key maps with no React and no
2
+ // other import. A framework layer (the React parts beside this folder) imports from here; so can any other code.
3
+ export { numericWidthValue, primitiveAttributes } from './attributes.js';
4
+ export { isRowShown, LADDER_ROW_ATTRIBUTE, nextLadderRow, resolveLadderKey } from './ladder-keys.js';
5
+ export { clampFraction, fractionFromPointer, isActiveScrubPointer, isPrimaryScrubPointer, reduceScrubGesture, resolveScrubKey } from './scrubber-gesture.js';
6
+ export { DEPTH_HEIGHT, DEPTH_WIDTH, depthGeometry, depthLevelId, nextDepthIndex, resolveDepthKey } from './depth-profile.js';
7
+ export { DIFF_CHANGE_LABEL, compareExact, diffRowId, holdDiffOrder, nextDiffIndex, nextDiffSort, resolveDiffKey, signedDelta, sortDiffRows } from './diff-table.js';
8
+ export { TAPE_SIDE_LABEL, holdTape, newestFirst, nextTapeIndex, readTape, resolveTapeKey, tapeSummary, tapeTime } from './tape.js';
9
+ export { buildLadder } from './ladder.js';
10
+ export { receiptHash, receiptVerdict, resolveReceiptKey } from './receipt-rules.js';
11
+ export { buildSymbolIndex, nextSymbolIndex, normalizeSymbolText, resolveSymbolKey, searchSymbols } from './symbol-search.js';
12
+ export { barInterval, chartFrame, clampPriceRange, dataPriceSpan, growViewport, indexOfTime, lastBars, panPriceRange, panViewport, priceTicks, resolveChartKey, tickDecimals, timeOfIndex, zoomPriceRange, zoomViewport } from './chart-scale.js';
13
+ export { INITIAL_CHART_GESTURE, isChartGestureActive, reduceChartGesture } from './chart-gesture.js';
14
+ export { atr, bollinger, ema, rsi, sma, sourcePrice, studyRange, visibleIndexes, vwap, wma } from './studies.js';
15
+ export { indexRangeOfTime, volumeProfile } from './volume-profile.js';
16
+ export { ATTACH_LABELS, ATTACH_MODES, DRAWING_TOOLS, DRAWING_TOOL_LABELS, FIB_RATIOS, anchorCount, attachOf, barTime, barsFromFirst, barsFromLast, timeOfBarFromLast, describeAnchor, describeDrawing, drawingGeometry, drawingPriceRanges, editDrawing, hitTest, moveAnchor, moveDrawing, nextDrawingId, nudgeAmount, resolveDrawingKey, snapToBar } from './drawings.js';
17
+ export { describeBarsCsv, parseBarsCsv } from './csv-bars.js';
18
+ export { formatBarTime, heikinAshi } from './candles.js';
19
+ export { barField, barNote, fromPriceField, fromTimeField, parseBarField, toTimeField } from './drawing-fields.js';
20
+ export { describeSelection, sameSelection, selectionAfterClearTap, selectionAfterEscape, selectionAfterTap } from './selection.js';
21
+ export { AXIS_BOTTOM, MAIN_PANEL, describeMove, moveSeries, moveTargets, normalizeLayout, panelBands, panelOf, reconcileLayout } from './panels.js';
22
+ export { POINT_KINDS, alignSeries, duplicateIds, pointsFromValues, report, resolveSeries, seriesSetSchema, validateSeriesSet } from './series.js';
23
+ export { csvAdapter, toSeriesSet } from './adapter.js';
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Ladder keyboard contract. Read-only: no key places or changes an order.
3
+ *
4
+ * | Key | Action |
5
+ * |---------------|----------------------------------------------------------------|
6
+ * | ArrowDown | focus the next row down |
7
+ * | ArrowUp | focus the next row up |
8
+ * | Home | focus the best bid |
9
+ * | End | focus the best ask |
10
+ * | Enter / Space | seek to when this level last changed (when the host supplies it) |
11
+ *
12
+ * One row is the tab stop (roving tabindex), so Tab leaves the ladder in one press. Home and End are also the
13
+ * global transport "start" and "end" keys; inside the ladder they mean best bid and best ask, and the ladder
14
+ * stops them there.
15
+ */
16
+ export type LadderKeyAction = 'next' | 'previous' | 'best-bid' | 'best-ask';
17
+ /** The fields of a keyboard event the contract reads; a DOM or React KeyboardEvent fits. */
18
+ export interface LadderKeyInput {
19
+ key: string;
20
+ altKey: boolean;
21
+ ctrlKey: boolean;
22
+ metaKey: boolean;
23
+ shiftKey: boolean;
24
+ }
25
+ export declare function resolveLadderKey(event: LadderKeyInput): LadderKeyAction | null;
26
+ /** Marks a row for the contract. `best` rows are what Home and End jump to. */
27
+ export declare const LADDER_ROW_ATTRIBUTE = "data-ladder-row";
28
+ /**
29
+ * On screen: it has a box and no closed `<details>` above it. Chromium still reports boxes for the contents of a
30
+ * closed disclosure, so the box test alone is not enough.
31
+ */
32
+ export declare function isRowShown(row: HTMLElement): boolean;
33
+ /** The row to focus, or null when the key does nothing here. `rows` are in screen order, top to bottom. */
34
+ export declare function nextLadderRow(rows: readonly HTMLElement[], current: HTMLElement | null, action: LadderKeyAction): HTMLElement | null;
35
+ //# sourceMappingURL=ladder-keys.d.ts.map
@@ -0,0 +1,43 @@
1
+ export function resolveLadderKey(event) {
2
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey)
3
+ return null;
4
+ switch (event.key) {
5
+ case 'ArrowDown':
6
+ return 'next';
7
+ case 'ArrowUp':
8
+ return 'previous';
9
+ case 'Home':
10
+ return 'best-bid';
11
+ case 'End':
12
+ return 'best-ask';
13
+ default:
14
+ return null;
15
+ }
16
+ }
17
+ /** Marks a row for the contract. `best` rows are what Home and End jump to. */
18
+ export const LADDER_ROW_ATTRIBUTE = 'data-ladder-row';
19
+ /**
20
+ * On screen: it has a box and no closed `<details>` above it. Chromium still reports boxes for the contents of a
21
+ * closed disclosure, so the box test alone is not enough.
22
+ */
23
+ export function isRowShown(row) {
24
+ for (let el = row.parentElement; el; el = el.parentElement) {
25
+ if (el instanceof HTMLDetailsElement && !el.open)
26
+ return false;
27
+ }
28
+ return row.getClientRects().length > 0;
29
+ }
30
+ /** The row to focus, or null when the key does nothing here. `rows` are in screen order, top to bottom. */
31
+ export function nextLadderRow(rows, current, action) {
32
+ if (!rows.length)
33
+ return null;
34
+ if (action === 'best-bid')
35
+ return rows.find((row) => row.dataset.best === 'bid') ?? null;
36
+ if (action === 'best-ask')
37
+ return rows.find((row) => row.dataset.best === 'ask') ?? null;
38
+ const at = current ? rows.indexOf(current) : -1;
39
+ if (at < 0)
40
+ return rows[0] ?? null;
41
+ const to = action === 'next' ? at + 1 : at - 1;
42
+ return rows[to] ?? null; // stops at the ends; never wraps
43
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The rules of the price ladder as plain functions. Prices and sizes arrive as exact decimal strings and are summed and
3
+ * grouped as scaled integers, never as floats; the only number made here is a bar's width, 0 to 1. A level that breaks a
4
+ * rule is reported and left out. A crossed book is reported and still drawn as given: nothing here repairs a book.
5
+ */
6
+ export interface LadderLevelIn {
7
+ /** Exact decimal string, above zero. */
8
+ price: string;
9
+ /** Exact decimal string, above zero. */
10
+ size: string;
11
+ }
12
+ export interface LadderInput {
13
+ bids: readonly LadderLevelIn[];
14
+ asks: readonly LadderLevelIn[];
15
+ /** The grouping step, an exact decimal string above zero: `0.25` groups prices into quarters. Bids round down, asks round up, so grouping never crosses the spread. */
16
+ step: string;
17
+ /** Rows shown on each side, nearest the spread. Default 10. */
18
+ levels?: number;
19
+ /** The last trade price, for the divider row. */
20
+ last?: string;
21
+ /** Size traded at each price, for the traded column. A trade at or above the best ask is counted in the ask row's bucket, a lower one in the bid row's. */
22
+ traded?: readonly LadderLevelIn[];
23
+ }
24
+ export type LadderSide = 'bid' | 'ask';
25
+ export interface LadderRow {
26
+ /** `side:price`, the row's identity (the price, not the position). */
27
+ id: string;
28
+ side: LadderSide;
29
+ /** The bucket's price, with as many decimal places as the step. */
30
+ price: string;
31
+ /** Total size in the bucket, with as many places as the most precise size given. */
32
+ size: string;
33
+ /** Size traded at this price, when any was. */
34
+ traded?: string;
35
+ /** Bar width: this row's size over the largest row shown, 0 to 1. */
36
+ fraction: number;
37
+ /** The best bid or the best ask. */
38
+ best: boolean;
39
+ }
40
+ export interface LadderIssue {
41
+ where: 'bids' | 'asks' | 'traded' | 'step' | 'last' | 'book';
42
+ /** Place in that list, when the problem is one entry. */
43
+ index?: number;
44
+ message: string;
45
+ }
46
+ export interface LadderBuilt {
47
+ /** Top to bottom: highest ask first, lowest ask last. */
48
+ asks: LadderRow[];
49
+ /** Top to bottom: highest bid first. */
50
+ bids: LadderRow[];
51
+ /** Asks then bids, the order a ladder is drawn. */
52
+ rows: LadderRow[];
53
+ /** The divider between asks and bids: the last price and the spread, each only when it can be said. */
54
+ last: {
55
+ price?: string;
56
+ spread?: string;
57
+ };
58
+ issues: LadderIssue[];
59
+ }
60
+ /**
61
+ * Group the book into rows. Bids and asks are grouped by `step`, summed exactly, trimmed to `levels` rows a side, and
62
+ * given a bar width. Nothing is drawn for a level that breaks a rule; `issues` says which.
63
+ */
64
+ export declare function buildLadder(input: LadderInput): LadderBuilt;
65
+ //# sourceMappingURL=ladder.d.ts.map
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The rules of the price ladder as plain functions. Prices and sizes arrive as exact decimal strings and are summed and
3
+ * grouped as scaled integers, never as floats; the only number made here is a bar's width, 0 to 1. A level that breaks a
4
+ * rule is reported and left out. A crossed book is reported and still drawn as given: nothing here repairs a book.
5
+ */
6
+ const POSITIVE = /^(\d+)(?:\.(\d+))?$/;
7
+ function parse(value) {
8
+ if (typeof value !== 'string')
9
+ return null;
10
+ const m = POSITIVE.exec(value);
11
+ if (!m || !/[1-9]/.test(value))
12
+ return null;
13
+ const places = m[2]?.length ?? 0;
14
+ return { units: BigInt(m[1] + (m[2] ?? '')), places };
15
+ }
16
+ const rescale = (d, places) => d.units * 10n ** BigInt(places - d.places);
17
+ function show(units, places) {
18
+ const digits = units.toString().padStart(places + 1, '0');
19
+ return places === 0 ? digits : `${digits.slice(0, -places)}.${digits.slice(-places)}`;
20
+ }
21
+ function readLevels(list, where, issues) {
22
+ const out = [];
23
+ (list ?? []).forEach((item, index) => {
24
+ const price = parse(item?.price);
25
+ const size = parse(item?.size);
26
+ if (!price || !size) {
27
+ issues.push({ where, index, message: `${!price ? 'price' : 'size'} must be an exact decimal string above zero` });
28
+ return;
29
+ }
30
+ out.push({ index, price, size });
31
+ });
32
+ return out;
33
+ }
34
+ /**
35
+ * Group the book into rows. Bids and asks are grouped by `step`, summed exactly, trimmed to `levels` rows a side, and
36
+ * given a bar width. Nothing is drawn for a level that breaks a rule; `issues` says which.
37
+ */
38
+ export function buildLadder(input) {
39
+ const issues = [];
40
+ const empty = { asks: [], bids: [], rows: [], last: {}, issues };
41
+ const step = parse(input.step);
42
+ if (!step) {
43
+ issues.push({ where: 'step', message: 'step must be an exact decimal string above zero' });
44
+ return empty;
45
+ }
46
+ const bids = readLevels(input.bids, 'bids', issues);
47
+ const asks = readLevels(input.asks, 'asks', issues);
48
+ const traded = readLevels(input.traded, 'traded', issues);
49
+ const places = Math.max(step.places, ...bids.concat(asks, traded).map((g) => g.price.places));
50
+ const unit = rescale(step, places);
51
+ const sizePlaces = Math.max(0, ...bids.concat(asks, traded).map((g) => g.size.places));
52
+ const floor = (p) => (p / unit) * unit;
53
+ const ceil = (p) => ((p + unit - 1n) / unit) * unit;
54
+ const bucket = (list, round) => {
55
+ const sums = new Map();
56
+ for (const g of list) {
57
+ const key = round(rescale(g.price, places));
58
+ sums.set(key, (sums.get(key) ?? 0n) + rescale(g.size, sizePlaces));
59
+ }
60
+ return sums;
61
+ };
62
+ const descending = (a, b) => (a < b ? 1 : a > b ? -1 : 0);
63
+ const take = Math.max(1, Math.floor(input.levels ?? 10));
64
+ const bidBuckets = [...bucket(bids, floor)].sort((a, b) => descending(a[0], b[0])).slice(0, take);
65
+ const askBuckets = [...bucket(asks, ceil)].sort((a, b) => descending(b[0], a[0])).slice(0, take); // lowest first, nearest the spread
66
+ const bestBid = bids.length ? bids.reduce((m, g) => (rescale(g.price, places) > m ? rescale(g.price, places) : m), 0n) : null;
67
+ const bestAsk = asks.length ? asks.reduce((m, g) => { const p = rescale(g.price, places); return m === null || p < m ? p : m; }, null) : null;
68
+ if (bestBid !== null && bestAsk !== null && bestBid >= bestAsk) {
69
+ issues.push({ where: 'book', message: `crossed book: best bid ${show(bestBid, places)} is not below best ask ${show(bestAsk, places)}` });
70
+ }
71
+ // Traded size: at or above the best ask it belongs to an ask row, below it to a bid row.
72
+ const tradedUp = bucket(traded.filter((g) => bestAsk !== null && rescale(g.price, places) >= bestAsk), ceil);
73
+ const tradedDown = bucket(traded.filter((g) => bestAsk === null || rescale(g.price, places) < bestAsk), floor);
74
+ const largest = [...bidBuckets, ...askBuckets].reduce((m, [, s]) => (s > m ? s : m), 0n);
75
+ const bar = (s) => (largest === 0n ? 0 : Number((s * 10000n) / largest) / 10000);
76
+ const make = (side, [key, size], best, tradedMap) => {
77
+ const price = show(key / 10n ** BigInt(places - step.places), step.places); // a bucket key is a whole number of steps, so this divides exactly
78
+ const t = tradedMap.get(key);
79
+ return { id: `${side}:${price}`, side, price, size: show(size, sizePlaces), traded: t === undefined ? undefined : show(t, sizePlaces), fraction: bar(size), best };
80
+ };
81
+ const askRows = askBuckets.map((b, i) => make('ask', b, i === 0, tradedUp)).reverse(); // drawn highest first
82
+ const bidRows = bidBuckets.map((b, i) => make('bid', b, i === 0, tradedDown));
83
+ const last = {};
84
+ if (input.last !== undefined) {
85
+ if (parse(input.last))
86
+ last.price = input.last;
87
+ else
88
+ issues.push({ where: 'last', message: 'last must be an exact decimal string above zero' });
89
+ }
90
+ if (bestBid !== null && bestAsk !== null && bestAsk > bestBid)
91
+ last.spread = show(bestAsk - bestBid, places);
92
+ return { asks: askRows, bids: bidRows, rows: [...askRows, ...bidRows], last, issues };
93
+ }
@@ -0,0 +1,72 @@
1
+ /** The id of the first band. It always exists, is always first, and holds the chart's own bars and volume. */
2
+ export declare const MAIN_PANEL = "main";
3
+ export interface PanelSpec {
4
+ id: string;
5
+ /** What the band is called in lists and live-region text. */
6
+ label: string;
7
+ /** Share of the height. Default 1 for a panel and 3 for main. */
8
+ weight?: number;
9
+ }
10
+ export interface PanelLayout {
11
+ /** The bands from top to bottom. The first is always `main`. */
12
+ panels: readonly PanelSpec[];
13
+ /** Series id to panel id. A series with no entry, or a panel that is gone, is in `main`. */
14
+ placement: Readonly<Record<string, string>>;
15
+ }
16
+ export interface PanelBand {
17
+ id: string;
18
+ /** Top edge, from the top of the plot. */
19
+ top: number;
20
+ height: number;
21
+ }
22
+ /** Height kept under the bands for the time labels: the same as the chart frame's default `axisBottom`. */
23
+ export declare const AXIS_BOTTOM = 18;
24
+ /** A layout with only `main` (and the given bands below it), `main` forced to the front. */
25
+ export declare function normalizeLayout(layout: PanelLayout): PanelLayout;
26
+ /** The panel a series is in: its placement when that panel exists, otherwise `main`. */
27
+ export declare function panelOf(layout: PanelLayout, seriesId: string): string;
28
+ /**
29
+ * Split `height` into bands by weight, top to bottom, with no gaps. A band is never shorter than 40 while the height
30
+ * allows it; with too little height the bands share what there is.
31
+ */
32
+ export declare function panelBands(panels: readonly PanelSpec[], height: number): PanelBand[];
33
+ /** Where a series can go: an existing panel, or a new one beside the panel it is in. */
34
+ export type MoveDestination = {
35
+ kind: 'panel';
36
+ panelId: string;
37
+ } | {
38
+ kind: 'new';
39
+ side: 'above' | 'below';
40
+ beside: string;
41
+ };
42
+ export interface MoveTarget {
43
+ destination: MoveDestination;
44
+ /** The words for a button, for example "Move up to Main" or "Move to a new panel below". */
45
+ label: string;
46
+ }
47
+ /**
48
+ * The destinations for one series, relative to where it is: the panel above, the panel below, a new panel above, a new
49
+ * panel below. A destination that would change nothing is left out (a lone series in a panel gains nothing from a new
50
+ * panel beside its own), and so is "new panel above" for `main`, which stays first.
51
+ */
52
+ export declare function moveTargets(layout: PanelLayout, seriesId: string): MoveTarget[];
53
+ /**
54
+ * Move a series. Returns the same layout object when nothing changes. A panel left with no series is removed (never
55
+ * `main`). The result is a new layout: the arguments are not changed.
56
+ */
57
+ export declare function moveSeries(layout: PanelLayout, seriesId: string, destination: MoveDestination): PanelLayout;
58
+ /** The words for a live region after a move, for example "Moved RSI to Panel 2." */
59
+ export declare function describeMove(label: string, panelLabel: string): string;
60
+ /** A series as `reconcileLayout` reads it: its id and the panel it asks to start in. */
61
+ export interface PanelSeriesRequest {
62
+ id: string;
63
+ panel?: string;
64
+ }
65
+ /**
66
+ * Bring an uncontrolled layout in step with a changed list of series, keeping every placement a person already made.
67
+ * A series that is new (not in `seen`) starts in the panel it names, which is created below `main` when it does not
68
+ * exist (labelled from `named`, else its id). A series that is gone loses its placement, and a panel left with no
69
+ * series is removed. Returns the same layout when nothing changes.
70
+ */
71
+ export declare function reconcileLayout(layout: PanelLayout, series: readonly PanelSeriesRequest[], seen: readonly string[], named?: readonly PanelSpec[]): PanelLayout;
72
+ //# sourceMappingURL=panels.d.ts.map
@@ -0,0 +1,151 @@
1
+ // Panels: a chart is one or more vertical bands, each with its own price scale, sharing one time axis. The bars (and the
2
+ // volume under them) stay in the first band, "main". Other series are placed in a band by id, and move between bands by
3
+ // relative destinations ("up", "down", "new panel above or below"), which is the single-pointer path WCAG 2.2 (2.5.7)
4
+ // asks of any drag. The rules are plain functions so any framework layer, and a test, can use them.
5
+ /** The id of the first band. It always exists, is always first, and holds the chart's own bars and volume. */
6
+ export const MAIN_PANEL = 'main';
7
+ /** Height kept under the bands for the time labels: the same as the chart frame's default `axisBottom`. */
8
+ export const AXIS_BOTTOM = 18;
9
+ const MIN_BAND = 40;
10
+ const MAIN_WEIGHT = 3;
11
+ const weightOf = (panel) => (Number.isFinite(panel.weight) && panel.weight > 0 ? panel.weight : panel.id === MAIN_PANEL ? MAIN_WEIGHT : 1);
12
+ /** A layout with only `main` (and the given bands below it), `main` forced to the front. */
13
+ export function normalizeLayout(layout) {
14
+ const seen = new Set();
15
+ const rest = [];
16
+ let main = { id: MAIN_PANEL, label: 'Main' };
17
+ for (const panel of layout.panels ?? []) {
18
+ if (!panel || typeof panel.id !== 'string' || seen.has(panel.id))
19
+ continue;
20
+ seen.add(panel.id);
21
+ if (panel.id === MAIN_PANEL)
22
+ main = { ...main, ...panel };
23
+ else
24
+ rest.push(panel);
25
+ }
26
+ return { panels: [main, ...rest], placement: layout.placement ?? {} };
27
+ }
28
+ /** The panel a series is in: its placement when that panel exists, otherwise `main`. */
29
+ export function panelOf(layout, seriesId) {
30
+ const wanted = layout.placement[seriesId];
31
+ return wanted !== undefined && layout.panels.some((p) => p.id === wanted) ? wanted : MAIN_PANEL;
32
+ }
33
+ /**
34
+ * Split `height` into bands by weight, top to bottom, with no gaps. A band is never shorter than 40 while the height
35
+ * allows it; with too little height the bands share what there is.
36
+ */
37
+ export function panelBands(panels, height) {
38
+ const total = Number.isFinite(height) && height > 0 ? height : 1;
39
+ const weights = panels.map(weightOf);
40
+ const sum = weights.reduce((a, b) => a + b, 0) || 1;
41
+ const floor = Math.min(MIN_BAND, total / Math.max(1, panels.length));
42
+ let heights = weights.map((w) => Math.max(floor, (total * w) / sum));
43
+ // Taking the floor from some bands can overshoot; scale the bands that were above it back down.
44
+ const over = heights.reduce((a, b) => a + b, 0) - total;
45
+ if (over > 0.001) {
46
+ const spare = heights.reduce((a, h) => a + Math.max(0, h - floor), 0);
47
+ if (spare > 0)
48
+ heights = heights.map((h) => h - (Math.max(0, h - floor) / spare) * over);
49
+ }
50
+ let top = 0;
51
+ return panels.map((p, i) => {
52
+ const h = i === panels.length - 1 ? Math.max(0, total - top) : Math.round(heights[i] * 100) / 100;
53
+ const band = { id: p.id, top: Math.round(top * 100) / 100, height: h };
54
+ top += h;
55
+ return band;
56
+ });
57
+ }
58
+ /**
59
+ * The destinations for one series, relative to where it is: the panel above, the panel below, a new panel above, a new
60
+ * panel below. A destination that would change nothing is left out (a lone series in a panel gains nothing from a new
61
+ * panel beside its own), and so is "new panel above" for `main`, which stays first.
62
+ */
63
+ export function moveTargets(layout, seriesId) {
64
+ const here = panelOf(layout, seriesId);
65
+ const at = layout.panels.findIndex((p) => p.id === here);
66
+ const out = [];
67
+ const above = layout.panels[at - 1];
68
+ const below = layout.panels[at + 1];
69
+ if (above)
70
+ out.push({ destination: { kind: 'panel', panelId: above.id }, label: `Move up to ${above.label}` });
71
+ if (below)
72
+ out.push({ destination: { kind: 'panel', panelId: below.id }, label: `Move down to ${below.label}` });
73
+ const alone = here !== MAIN_PANEL && layout.panels.length > 0 && Object.keys(layout.placement).filter((id) => panelOf(layout, id) === here).length <= 1;
74
+ if (here !== MAIN_PANEL && !alone)
75
+ out.push({ destination: { kind: 'new', side: 'above', beside: here }, label: 'Move to a new panel above' });
76
+ if (!alone)
77
+ out.push({ destination: { kind: 'new', side: 'below', beside: here }, label: 'Move to a new panel below' });
78
+ return out;
79
+ }
80
+ const nextPanelId = (panels) => {
81
+ let n = panels.length;
82
+ const ids = new Set(panels.map((p) => p.id));
83
+ while (ids.has(`panel-${n}`))
84
+ n++;
85
+ return { id: `panel-${n}`, label: `Panel ${n}` };
86
+ };
87
+ /**
88
+ * Move a series. Returns the same layout object when nothing changes. A panel left with no series is removed (never
89
+ * `main`). The result is a new layout: the arguments are not changed.
90
+ */
91
+ export function moveSeries(layout, seriesId, destination) {
92
+ const here = panelOf(layout, seriesId);
93
+ let panels = [...layout.panels];
94
+ let target;
95
+ if (destination.kind === 'panel') {
96
+ if (!panels.some((p) => p.id === destination.panelId) || destination.panelId === here)
97
+ return layout;
98
+ target = destination.panelId;
99
+ }
100
+ else {
101
+ const beside = panels.findIndex((p) => p.id === destination.beside);
102
+ if (beside < 0)
103
+ return layout;
104
+ if (destination.side === 'above' && panels[beside].id === MAIN_PANEL)
105
+ return layout; // main stays first
106
+ const made = nextPanelId(panels);
107
+ panels.splice(destination.side === 'above' ? beside : beside + 1, 0, { id: made.id, label: made.label });
108
+ target = made.id;
109
+ }
110
+ const placement = { ...layout.placement, [seriesId]: target };
111
+ const used = new Set(Object.keys(placement).map((id) => panelOf({ panels, placement }, id)));
112
+ panels = panels.filter((p) => p.id === MAIN_PANEL || used.has(p.id) || p.id === target);
113
+ return { panels, placement };
114
+ }
115
+ /** The words for a live region after a move, for example "Moved RSI to Panel 2." */
116
+ export function describeMove(label, panelLabel) {
117
+ return `Moved ${label} to ${panelLabel}.`;
118
+ }
119
+ /**
120
+ * Bring an uncontrolled layout in step with a changed list of series, keeping every placement a person already made.
121
+ * A series that is new (not in `seen`) starts in the panel it names, which is created below `main` when it does not
122
+ * exist (labelled from `named`, else its id). A series that is gone loses its placement, and a panel left with no
123
+ * series is removed. Returns the same layout when nothing changes.
124
+ */
125
+ export function reconcileLayout(layout, series, seen, named = []) {
126
+ const now = new Set(series.map((s) => s.id));
127
+ const before = new Set(seen);
128
+ let panels = [...layout.panels];
129
+ const placement = { ...layout.placement };
130
+ let changed = false;
131
+ for (const id of seen)
132
+ if (!now.has(id) && id in placement) {
133
+ delete placement[id];
134
+ changed = true;
135
+ }
136
+ for (const s of series) {
137
+ if (before.has(s.id) || !s.panel || s.panel === MAIN_PANEL)
138
+ continue;
139
+ if (!panels.some((p) => p.id === s.panel))
140
+ panels.push(named.find((n) => n.id === s.panel) ?? { id: s.panel, label: s.panel });
141
+ placement[s.id] = s.panel;
142
+ changed = true;
143
+ }
144
+ const used = new Set(Object.keys(placement).map((id) => placement[id]));
145
+ const kept = panels.filter((p) => p.id === MAIN_PANEL || used.has(p.id));
146
+ if (kept.length !== panels.length) {
147
+ panels = kept;
148
+ changed = true;
149
+ }
150
+ return changed ? { panels, placement } : layout;
151
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The rules of the evidence receipt as plain functions. Digests and sizes are strings and are
3
+ * printed as given.
4
+ */
5
+ export interface ReceiptData {
6
+ dataset: string;
7
+ symbol: string;
8
+ synthetic: boolean;
9
+ a: string;
10
+ b: string;
11
+ eventsBetween: number;
12
+ sourceDigest: string;
13
+ stateDigestA: string;
14
+ stateDigestB: string;
15
+ resultHash: string;
16
+ engineVersion: string;
17
+ levelsDiffer: number;
18
+ /** Whether replaying from the start gave the same two states and the same result hash. */
19
+ recomputeMatch: boolean;
20
+ /** The line that must travel with a copy when the data is not synthetic. */
21
+ shareNote?: string;
22
+ }
23
+ export type ReceiptVerdict = 'match' | 'mismatch';
24
+ export declare const receiptVerdict: (receipt: Pick<ReceiptData, "recomputeMatch">) => ReceiptVerdict;
25
+ export type ReceiptHashOf = 'source' | 'a' | 'b' | 'result';
26
+ export declare function receiptHash(receipt: ReceiptData, of: ReceiptHashOf): string;
27
+ interface KeyLike {
28
+ key: string;
29
+ ctrlKey: boolean;
30
+ metaKey: boolean;
31
+ altKey: boolean;
32
+ }
33
+ /**
34
+ * Enter or `y` copies the result hash. Enter on a button or link is that control's own and is left alone; `y` is
35
+ * ignored while typing in a field.
36
+ */
37
+ export declare function resolveReceiptKey(event: KeyLike, target: {
38
+ tagName: string;
39
+ isContentEditable?: boolean;
40
+ }): 'copy-hash' | null;
41
+ export {};
42
+ //# sourceMappingURL=receipt-rules.d.ts.map
@@ -0,0 +1,20 @@
1
+ export const receiptVerdict = (receipt) => (receipt.recomputeMatch ? 'match' : 'mismatch');
2
+ export function receiptHash(receipt, of) {
3
+ return of === 'source' ? receipt.sourceDigest : of === 'a' ? receipt.stateDigestA : of === 'b' ? receipt.stateDigestB : receipt.resultHash;
4
+ }
5
+ /**
6
+ * Enter or `y` copies the result hash. Enter on a button or link is that control's own and is left alone; `y` is
7
+ * ignored while typing in a field.
8
+ */
9
+ export function resolveReceiptKey(event, target) {
10
+ if (event.ctrlKey || event.metaKey || event.altKey)
11
+ return null;
12
+ const tag = target.tagName.toLowerCase();
13
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable)
14
+ return null;
15
+ if (event.key === 'y')
16
+ return 'copy-hash';
17
+ if (event.key === 'Enter' && tag !== 'button' && tag !== 'a')
18
+ return 'copy-hash';
19
+ return null;
20
+ }