@forgecharts/sdk 1.1.23
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.
- package/package.json +50 -0
- package/src/__tests__/backwardCompatibility.test.ts +191 -0
- package/src/__tests__/candleInvariant.test.ts +500 -0
- package/src/__tests__/public-api-surface.ts +76 -0
- package/src/__tests__/timeframeBoundary.test.ts +583 -0
- package/src/api/DrawingManager.ts +188 -0
- package/src/api/EventBus.ts +53 -0
- package/src/api/IndicatorDAG.ts +389 -0
- package/src/api/IndicatorRegistry.ts +47 -0
- package/src/api/LayoutManager.ts +72 -0
- package/src/api/PaneManager.ts +129 -0
- package/src/api/ReferenceAPI.ts +195 -0
- package/src/api/TChart.ts +881 -0
- package/src/api/createChart.ts +43 -0
- package/src/api/drawing tools/fib gann menu/fibRetracement.ts +27 -0
- package/src/api/drawing tools/lines menu/crossLine.ts +21 -0
- package/src/api/drawing tools/lines menu/disjointChannel.ts +74 -0
- package/src/api/drawing tools/lines menu/extendedLine.ts +22 -0
- package/src/api/drawing tools/lines menu/flatTopBottom.ts +45 -0
- package/src/api/drawing tools/lines menu/horizontal.ts +24 -0
- package/src/api/drawing tools/lines menu/horizontalRay.ts +25 -0
- package/src/api/drawing tools/lines menu/infoLine.ts +127 -0
- package/src/api/drawing tools/lines menu/insidePitchfork.ts +21 -0
- package/src/api/drawing tools/lines menu/modifiedSchiffPitchfork.ts +18 -0
- package/src/api/drawing tools/lines menu/parallelChannel.ts +47 -0
- package/src/api/drawing tools/lines menu/pitchfork.ts +15 -0
- package/src/api/drawing tools/lines menu/ray.ts +28 -0
- package/src/api/drawing tools/lines menu/regressionTrend.ts +157 -0
- package/src/api/drawing tools/lines menu/schiffPitchfork.ts +18 -0
- package/src/api/drawing tools/lines menu/trendAngle.ts +64 -0
- package/src/api/drawing tools/lines menu/trendline.ts +16 -0
- package/src/api/drawing tools/lines menu/vertical.ts +16 -0
- package/src/api/drawing tools/pointers menu/crosshair.ts +17 -0
- package/src/api/drawing tools/pointers menu/cursor.ts +16 -0
- package/src/api/drawing tools/pointers menu/demonstration.ts +35 -0
- package/src/api/drawing tools/pointers menu/dot.ts +26 -0
- package/src/api/drawing tools/shapes menu/rectangle.ts +24 -0
- package/src/api/drawing tools/shapes menu/text.ts +30 -0
- package/src/api/drawingUtils.ts +82 -0
- package/src/core/CanvasLayer.ts +77 -0
- package/src/core/Chart.ts +917 -0
- package/src/core/CoordTransform.ts +282 -0
- package/src/core/Crosshair.ts +207 -0
- package/src/core/IndicatorEngine.ts +216 -0
- package/src/core/InteractionManager.ts +899 -0
- package/src/core/PriceScale.ts +133 -0
- package/src/core/Series.ts +132 -0
- package/src/core/TimeScale.ts +175 -0
- package/src/datafeed/DatafeedConnector.ts +300 -0
- package/src/engine/CandleEngine.ts +458 -0
- package/src/engine/__tests__/CandleEngine.test.ts +402 -0
- package/src/engine/candleInvariants.ts +172 -0
- package/src/engine/mergeUtils.ts +93 -0
- package/src/engine/timeframeUtils.ts +118 -0
- package/src/index.ts +190 -0
- package/src/internal.ts +41 -0
- package/src/licensing/ChartRuntimeResolver.ts +380 -0
- package/src/licensing/LicenseManager.ts +131 -0
- package/src/licensing/__tests__/ChartRuntimeResolver.test.ts +207 -0
- package/src/licensing/__tests__/LicenseManager.test.ts +180 -0
- package/src/licensing/licenseTypes.ts +19 -0
- package/src/pine/PineCompiler.ts +68 -0
- package/src/pine/diagnostics.ts +30 -0
- package/src/pine/index.ts +7 -0
- package/src/pine/pine-ast.ts +163 -0
- package/src/pine/pine-lexer.ts +265 -0
- package/src/pine/pine-parser.ts +439 -0
- package/src/pine/pine-transpiler.ts +301 -0
- package/src/pixi/LayerName.ts +35 -0
- package/src/pixi/PixiCandlestickRenderer.ts +125 -0
- package/src/pixi/PixiChart.ts +425 -0
- package/src/pixi/PixiCrosshairRenderer.ts +134 -0
- package/src/pixi/PixiDrawingRenderer.ts +121 -0
- package/src/pixi/PixiGridRenderer.ts +136 -0
- package/src/pixi/PixiLayerManager.ts +102 -0
- package/src/renderers/CandlestickRenderer.ts +130 -0
- package/src/renderers/HistogramRenderer.ts +63 -0
- package/src/renderers/LineRenderer.ts +77 -0
- package/src/theme/colors.ts +21 -0
- package/src/tools/barDivergenceCheck.ts +305 -0
- package/src/trading/TradingOverlayStore.ts +161 -0
- package/src/trading/UnmanagedIngestion.ts +156 -0
- package/src/trading/__tests__/ManagedTradingController.test.ts +338 -0
- package/src/trading/__tests__/TradingOverlayStore.test.ts +323 -0
- package/src/trading/__tests__/UnmanagedIngestion.test.ts +205 -0
- package/src/trading/managed/ManagedTradingController.ts +292 -0
- package/src/trading/managed/managedCapabilities.ts +98 -0
- package/src/trading/managed/managedTypes.ts +151 -0
- package/src/trading/tradingTypes.ts +135 -0
- package/src/tscript/TScriptIndicator.ts +54 -0
- package/src/tscript/ast.ts +105 -0
- package/src/tscript/lexer.ts +190 -0
- package/src/tscript/parser.ts +334 -0
- package/src/tscript/runtime.ts +525 -0
- package/src/tscript/series.ts +84 -0
- package/src/types/IChart.ts +56 -0
- package/src/types/IRenderer.ts +16 -0
- package/src/types/ISeries.ts +30 -0
- package/tsconfig.json +22 -0
- package/tsup.config.ts +15 -0
- package/vitest.config.ts +25 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candle Invariant Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the invariant guards in `candleInvariants.ts` fire correctly,
|
|
5
|
+
* and that `CandleEngine.applyLiveUpdate` maintains all invariants during
|
|
6
|
+
* normal operation (no warnings) and surfaces violations during abnormal
|
|
7
|
+
* conditions (mocked guard paths).
|
|
8
|
+
*
|
|
9
|
+
* Test categories:
|
|
10
|
+
* 1. assertOhlcRelationships — each OHLC violation is detected independently
|
|
11
|
+
* 2. assertBucketAlignment — misaligned timestamps are caught
|
|
12
|
+
* 3. assertNotFinalized — mutating a closed bar triggers the guard
|
|
13
|
+
* 4. assertOpenImmutability — open-price overwrite is detected
|
|
14
|
+
* 5. assertNoDuplicateAppend — same-bucket spurious append is detected
|
|
15
|
+
* 6. assertFinalizedUnchanged — new-bucket finalisation is verified
|
|
16
|
+
* 7. CandleEngine integration — no spurious warnings during correct operation
|
|
17
|
+
* 8. barDivergenceCheck — compareSources and formatReport
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
21
|
+
import {
|
|
22
|
+
assertOhlcRelationships,
|
|
23
|
+
assertBucketAlignment,
|
|
24
|
+
assertNotFinalized,
|
|
25
|
+
assertOpenImmutability,
|
|
26
|
+
assertNoDuplicateAppend,
|
|
27
|
+
assertFinalizedUnchanged,
|
|
28
|
+
_setReporter,
|
|
29
|
+
_resetReporter,
|
|
30
|
+
} from '../engine/candleInvariants';
|
|
31
|
+
import type { CandleBar } from '../engine/CandleEngine';
|
|
32
|
+
import { CandleEngine } from '../engine/CandleEngine';
|
|
33
|
+
import type { RawOHLCV } from '../engine/CandleEngine';
|
|
34
|
+
import {
|
|
35
|
+
compareSources,
|
|
36
|
+
checkAllSources,
|
|
37
|
+
formatReport,
|
|
38
|
+
} from '../tools/barDivergenceCheck';
|
|
39
|
+
import type { SourceBars } from '../tools/barDivergenceCheck';
|
|
40
|
+
|
|
41
|
+
// ─── Reporter harness ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* During tests we capture invariant violation messages instead of printing
|
|
45
|
+
* them to console. This lets us assert on the exact message content.
|
|
46
|
+
*/
|
|
47
|
+
let violations: string[] = [];
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
violations = [];
|
|
51
|
+
_setReporter((msg) => violations.push(msg));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
_resetReporter();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/** Build a valid, closed CandleBar. */
|
|
61
|
+
function validBar(overrides: Partial<CandleBar> = {}): CandleBar {
|
|
62
|
+
return {
|
|
63
|
+
timeMs: 60_000,
|
|
64
|
+
time: 60,
|
|
65
|
+
open: 100,
|
|
66
|
+
high: 110,
|
|
67
|
+
low: 90,
|
|
68
|
+
close: 105,
|
|
69
|
+
volume: 1,
|
|
70
|
+
isClosed: true,
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Unix seconds helper. */
|
|
76
|
+
const sec = (iso: string): number => new Date(iso).getTime() / 1000;
|
|
77
|
+
|
|
78
|
+
/** Build a minimal RawOHLCV tick for the engine. */
|
|
79
|
+
function tick(
|
|
80
|
+
timeSec: number,
|
|
81
|
+
open = 100, high = 110, low = 90, close = 105, volume = 1,
|
|
82
|
+
): RawOHLCV {
|
|
83
|
+
return { time: timeSec, open, high, low, close, volume };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── 1. assertOhlcRelationships ───────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
describe('assertOhlcRelationships', () => {
|
|
89
|
+
it('passes for a valid OHLC bar with no violations', () => {
|
|
90
|
+
assertOhlcRelationships(validBar(), 'test');
|
|
91
|
+
expect(violations).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('detects high < open', () => {
|
|
95
|
+
assertOhlcRelationships(validBar({ open: 120, high: 110 }), 'ctx');
|
|
96
|
+
expect(violations.some(v => v.includes('high') && v.includes('open'))).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('detects high < close', () => {
|
|
100
|
+
assertOhlcRelationships(validBar({ close: 120, high: 110 }), 'ctx');
|
|
101
|
+
expect(violations.some(v => v.includes('high') && v.includes('close'))).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('detects high < low', () => {
|
|
105
|
+
assertOhlcRelationships(validBar({ high: 80, low: 90 }), 'ctx');
|
|
106
|
+
expect(violations.some(v => v.includes('high') && v.includes('low'))).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('detects low > open', () => {
|
|
110
|
+
assertOhlcRelationships(validBar({ open: 80, low: 90 }), 'ctx');
|
|
111
|
+
expect(violations.some(v => v.includes('low') && v.includes('open'))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('detects low > close', () => {
|
|
115
|
+
assertOhlcRelationships(validBar({ close: 80, low: 90 }), 'ctx');
|
|
116
|
+
expect(violations.some(v => v.includes('low') && v.includes('close'))).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('open === high === low === close is valid (doji candle)', () => {
|
|
120
|
+
assertOhlcRelationships(validBar({ open: 100, high: 100, low: 100, close: 100 }), 'ctx');
|
|
121
|
+
expect(violations).toHaveLength(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes context and timeMs in violation message', () => {
|
|
125
|
+
assertOhlcRelationships(validBar({ open: 120, high: 110, timeMs: 99_000 }), 'myCtx');
|
|
126
|
+
expect(violations[0]).toContain('[CANDLE_INVARIANT]');
|
|
127
|
+
expect(violations[0]).toContain('myCtx');
|
|
128
|
+
expect(violations[0]).toContain('99000');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ─── 2. assertBucketAlignment ─────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
describe('assertBucketAlignment', () => {
|
|
135
|
+
it('passes when timeMs is exactly on the 1m boundary', () => {
|
|
136
|
+
// 60_000 ms = exactly 1 minute from epoch — valid 1m bucket start
|
|
137
|
+
assertBucketAlignment(validBar({ timeMs: 60_000, time: 60 }), '1m', 'test');
|
|
138
|
+
expect(violations).toHaveLength(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('detects timeMs not on a 1m boundary', () => {
|
|
142
|
+
// 62_500 ms is NOT on a 60_000-ms boundary
|
|
143
|
+
assertBucketAlignment(validBar({ timeMs: 62_500, time: 62 }), '1m', 'test');
|
|
144
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
145
|
+
expect(violations[0]).toContain('timeMs');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('passes for 1h boundary', () => {
|
|
149
|
+
const hourStart = new Date('2025-03-09T14:00:00Z').getTime();
|
|
150
|
+
assertBucketAlignment(validBar({ timeMs: hourStart, time: hourStart / 1000 }), '1h', 'test');
|
|
151
|
+
expect(violations).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('detects misaligned 1h timestamp (30 min off)', () => {
|
|
155
|
+
const thirtyMin = new Date('2025-03-09T14:30:00Z').getTime();
|
|
156
|
+
assertBucketAlignment(validBar({ timeMs: thirtyMin, time: thirtyMin / 1000 }), '1h', 'test');
|
|
157
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('passes for a Monday 00:00 UTC timestamp with 1w', () => {
|
|
161
|
+
const monday = new Date('2025-03-03T00:00:00Z').getTime();
|
|
162
|
+
assertBucketAlignment(validBar({ timeMs: monday, time: monday / 1000 }), '1w', 'test');
|
|
163
|
+
expect(violations).toHaveLength(0);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('detects misaligned 1w timestamp (mid-week)', () => {
|
|
167
|
+
const wednesday = new Date('2025-03-05T12:00:00Z').getTime();
|
|
168
|
+
assertBucketAlignment(validBar({ timeMs: wednesday, time: wednesday / 1000 }), '1w', 'test');
|
|
169
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('passes for first-of-month timestamp with 1M', () => {
|
|
173
|
+
const monthStart = new Date('2025-03-01T00:00:00Z').getTime();
|
|
174
|
+
assertBucketAlignment(validBar({ timeMs: monthStart, time: monthStart / 1000 }), '1M', 'test');
|
|
175
|
+
expect(violations).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('detects mid-month timestamp with 1M', () => {
|
|
179
|
+
const midMonth = new Date('2025-03-15T00:00:00Z').getTime();
|
|
180
|
+
assertBucketAlignment(validBar({ timeMs: midMonth, time: midMonth / 1000 }), '1M', 'test');
|
|
181
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── 3. assertNotFinalized ────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe('assertNotFinalized', () => {
|
|
188
|
+
it('passes for an open (live) candle', () => {
|
|
189
|
+
assertNotFinalized(validBar({ isClosed: false }), 'test');
|
|
190
|
+
expect(violations).toHaveLength(0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('fires for a closed candle', () => {
|
|
194
|
+
assertNotFinalized(validBar({ isClosed: true }), 'test');
|
|
195
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
196
|
+
expect(violations[0]).toContain('finalized');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('violation message contains context and timeMs', () => {
|
|
200
|
+
assertNotFinalized(validBar({ isClosed: true, timeMs: 180_000 }), 'liveUpdate');
|
|
201
|
+
expect(violations[0]).toContain('liveUpdate');
|
|
202
|
+
expect(violations[0]).toContain('180000');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ─── 4. assertOpenImmutability ────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
describe('assertOpenImmutability', () => {
|
|
209
|
+
it('passes when open is unchanged', () => {
|
|
210
|
+
const bar = validBar({ open: 100 });
|
|
211
|
+
assertOpenImmutability(100, bar, 'test');
|
|
212
|
+
expect(violations).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('fires when open changed', () => {
|
|
216
|
+
const bar = validBar({ open: 200 });
|
|
217
|
+
assertOpenImmutability(100, bar, 'ctx');
|
|
218
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
219
|
+
expect(violations[0]).toContain('open');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('violation message shows previous and new open', () => {
|
|
223
|
+
const bar = validBar({ open: 200, timeMs: 120_000 });
|
|
224
|
+
assertOpenImmutability(100, bar, 'ctx');
|
|
225
|
+
expect(violations[0]).toContain('100');
|
|
226
|
+
expect(violations[0]).toContain('200');
|
|
227
|
+
expect(violations[0]).toContain('120000');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─── 5. assertNoDuplicateAppend ───────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
describe('assertNoDuplicateAppend', () => {
|
|
234
|
+
it('passes when count is unchanged', () => {
|
|
235
|
+
assertNoDuplicateAppend(5, 5, 'test');
|
|
236
|
+
expect(violations).toHaveLength(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('fires when count increased (spurious append)', () => {
|
|
240
|
+
assertNoDuplicateAppend(5, 6, 'ctx');
|
|
241
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
242
|
+
expect(violations[0]).toContain('same-bucket');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─── 6. assertFinalizedUnchanged ─────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
describe('assertFinalizedUnchanged', () => {
|
|
249
|
+
it('passes when the previous bar is now isClosed', () => {
|
|
250
|
+
const prev = validBar({ timeMs: 60_000, isClosed: false });
|
|
251
|
+
const current: CandleBar[] = [
|
|
252
|
+
{ ...prev, isClosed: true },
|
|
253
|
+
validBar({ timeMs: 120_000, isClosed: false }),
|
|
254
|
+
];
|
|
255
|
+
assertFinalizedUnchanged(prev, current, 'test');
|
|
256
|
+
expect(violations).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('fires when the previous bar is still open after a new-bucket append', () => {
|
|
260
|
+
const prev = validBar({ timeMs: 60_000, isClosed: false });
|
|
261
|
+
const current: CandleBar[] = [
|
|
262
|
+
{ ...prev, isClosed: false }, // not closed — violation
|
|
263
|
+
validBar({ timeMs: 120_000, isClosed: false }),
|
|
264
|
+
];
|
|
265
|
+
assertFinalizedUnchanged(prev, current, 'test');
|
|
266
|
+
expect(violations.length).toBeGreaterThan(0);
|
|
267
|
+
expect(violations[0]).toContain('isClosed');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('passes when the bar was removed (e.g. after a merge)', () => {
|
|
271
|
+
const prev = validBar({ timeMs: 60_000 });
|
|
272
|
+
// prev is not in `current` at all — should not fire
|
|
273
|
+
assertFinalizedUnchanged(prev, [], 'test');
|
|
274
|
+
expect(violations).toHaveLength(0);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ─── 7. CandleEngine integration — no spurious warnings ──────────────────────
|
|
279
|
+
|
|
280
|
+
describe('CandleEngine integration — clean operation produces no violations', () => {
|
|
281
|
+
const TF = '1m';
|
|
282
|
+
const T0 = sec('2025-03-09T14:00:00Z');
|
|
283
|
+
const T1 = sec('2025-03-09T14:01:00Z');
|
|
284
|
+
const T2 = sec('2025-03-09T14:02:00Z');
|
|
285
|
+
|
|
286
|
+
it('single tick produces no invariant violations', () => {
|
|
287
|
+
const engine = new CandleEngine();
|
|
288
|
+
engine.initialize([], TF as any);
|
|
289
|
+
engine.applyLiveUpdate(tick(T0, 100, 110, 90, 105));
|
|
290
|
+
expect(violations).toHaveLength(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('same-bucket updates produce no violations', () => {
|
|
294
|
+
const engine = new CandleEngine();
|
|
295
|
+
engine.initialize([], TF as any);
|
|
296
|
+
engine.applyLiveUpdate(tick(T0, 100, 110, 90, 105));
|
|
297
|
+
engine.applyLiveUpdate(tick(T0 + 30, 100, 120, 85, 115));
|
|
298
|
+
engine.applyLiveUpdate(tick(T0 + 45, 100, 120, 85, 112));
|
|
299
|
+
expect(violations).toHaveLength(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('new-bucket append produces no violations', () => {
|
|
303
|
+
const engine = new CandleEngine();
|
|
304
|
+
engine.initialize([], TF as any);
|
|
305
|
+
engine.applyLiveUpdate(tick(T0, 100, 110, 90, 105));
|
|
306
|
+
engine.applyLiveUpdate(tick(T1, 106, 120, 100, 115));
|
|
307
|
+
engine.applyLiveUpdate(tick(T2, 116, 125, 110, 120));
|
|
308
|
+
expect(violations).toHaveLength(0);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('backfillGap produces no violations', () => {
|
|
312
|
+
const T4 = sec('2025-03-09T14:04:00Z');
|
|
313
|
+
const engine = new CandleEngine();
|
|
314
|
+
engine.initialize([tick(T0, 100, 110, 90, 105)], TF as any);
|
|
315
|
+
engine.applyLiveUpdate(tick(T4, 130, 140, 120, 135));
|
|
316
|
+
engine.backfillGap([
|
|
317
|
+
tick(T1, 106, 115, 100, 112),
|
|
318
|
+
tick(T2, 112, 118, 105, 116),
|
|
319
|
+
]);
|
|
320
|
+
expect(violations).toHaveLength(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('handleReconnect produces no violations', () => {
|
|
324
|
+
const engine = new CandleEngine();
|
|
325
|
+
engine.initialize([
|
|
326
|
+
tick(T0, 100, 110, 90, 105),
|
|
327
|
+
tick(T1, 106, 112, 100, 110),
|
|
328
|
+
], TF as any);
|
|
329
|
+
engine.handleReconnect([
|
|
330
|
+
tick(T0, 100, 115, 88, 112),
|
|
331
|
+
tick(T1, 106, 118, 96, 115),
|
|
332
|
+
]);
|
|
333
|
+
expect(violations).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('weekly timeframe: calendar-aligned ticks produce no violations', () => {
|
|
337
|
+
const MONDAY = sec('2025-03-03T00:00:00Z');
|
|
338
|
+
const WEDNESDAY = sec('2025-03-05T12:00:00Z');
|
|
339
|
+
const FRIDAY = sec('2025-03-07T18:00:00Z');
|
|
340
|
+
|
|
341
|
+
const engine = new CandleEngine();
|
|
342
|
+
engine.initialize([], '1w' as any);
|
|
343
|
+
|
|
344
|
+
engine.applyLiveUpdate(tick(MONDAY, 40000, 42000, 39000, 41000));
|
|
345
|
+
// Wed and Fri are in the same 1w bucket — updates only
|
|
346
|
+
engine.applyLiveUpdate(tick(WEDNESDAY, 40000, 45000, 38000, 44000));
|
|
347
|
+
engine.applyLiveUpdate(tick(FRIDAY, 40000, 46000, 37500, 43000));
|
|
348
|
+
|
|
349
|
+
expect(violations).toHaveLength(0);
|
|
350
|
+
expect(engine.getBars().length).toBe(1); // still the same weekly candle
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('monthly timeframe: calendar-aligned ticks produce no violations', () => {
|
|
354
|
+
const MAR_01 = sec('2025-03-01T00:00:00Z');
|
|
355
|
+
const MAR_15 = sec('2025-03-15T12:00:00Z');
|
|
356
|
+
const APR_01 = sec('2025-04-01T00:00:00Z');
|
|
357
|
+
|
|
358
|
+
const engine = new CandleEngine();
|
|
359
|
+
engine.initialize([], '1M' as any);
|
|
360
|
+
|
|
361
|
+
engine.applyLiveUpdate(tick(MAR_01, 40000, 42000, 39000, 41000));
|
|
362
|
+
engine.applyLiveUpdate(tick(MAR_15, 40000, 45000, 38000, 44000)); // same month
|
|
363
|
+
engine.applyLiveUpdate(tick(APR_01, 44000, 46000, 43000, 45000));// new month
|
|
364
|
+
|
|
365
|
+
expect(violations).toHaveLength(0);
|
|
366
|
+
expect(engine.getBars().length).toBe(2); // March + April
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ─── 8. barDivergenceCheck ────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
describe('compareSources — identical sources', () => {
|
|
373
|
+
const BARS: SourceBars = {
|
|
374
|
+
source: 'provider',
|
|
375
|
+
bars: [
|
|
376
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 },
|
|
377
|
+
{ time: 1_000_060, open: 105, high: 115, low: 100, close: 112 },
|
|
378
|
+
{ time: 1_000_120, open: 112, high: 120, low: 108, close: 118 },
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
const DB_BARS: SourceBars = { source: 'database', bars: BARS.bars };
|
|
382
|
+
|
|
383
|
+
it('returns diverged=false when both sources are identical', () => {
|
|
384
|
+
const report = compareSources(BARS, DB_BARS, '1m', 'BTC', 1_000_000, 1_000_180);
|
|
385
|
+
expect(report.diverged).toBe(false);
|
|
386
|
+
expect(report.totalMismatches).toBe(0);
|
|
387
|
+
expect(report.firstMismatch).toBeNull();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('correct source counts', () => {
|
|
391
|
+
const report = compareSources(BARS, DB_BARS, '1m', 'BTC', 1_000_000, 1_000_180);
|
|
392
|
+
expect(report.sourceACount).toBe(3);
|
|
393
|
+
expect(report.sourceBCount).toBe(3);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('compareSources — OHLC divergence', () => {
|
|
398
|
+
it('detects first high mismatch and reports field name', () => {
|
|
399
|
+
const provider: SourceBars = {
|
|
400
|
+
source: 'provider',
|
|
401
|
+
bars: [
|
|
402
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 },
|
|
403
|
+
{ time: 1_000_060, open: 105, high: 115, low: 100, close: 112 },
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
const db: SourceBars = {
|
|
407
|
+
source: 'database',
|
|
408
|
+
bars: [
|
|
409
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 },
|
|
410
|
+
{ time: 1_000_060, open: 105, high: 999, low: 100, close: 112 }, // high differs
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
const report = compareSources(provider, db, '1m', 'BTC', 1_000_000, 1_000_120);
|
|
414
|
+
expect(report.diverged).toBe(true);
|
|
415
|
+
expect(report.firstMismatch).not.toBeNull();
|
|
416
|
+
expect(report.firstMismatch!.time).toBe(1_000_060);
|
|
417
|
+
expect(report.firstMismatch!.differences.some(d => d.field === 'high')).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('detects missing bar in one source', () => {
|
|
421
|
+
const provider: SourceBars = {
|
|
422
|
+
source: 'provider',
|
|
423
|
+
bars: [
|
|
424
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 },
|
|
425
|
+
{ time: 1_000_060, open: 105, high: 115, low: 100, close: 112 },
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
const db: SourceBars = {
|
|
429
|
+
source: 'database',
|
|
430
|
+
bars: [
|
|
431
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 },
|
|
432
|
+
// 1_000_060 is missing
|
|
433
|
+
],
|
|
434
|
+
};
|
|
435
|
+
const report = compareSources(provider, db, '1m', 'BTC', 1_000_000, 1_000_120);
|
|
436
|
+
expect(report.diverged).toBe(true);
|
|
437
|
+
expect(report.firstMismatch!.time).toBe(1_000_060);
|
|
438
|
+
expect(report.firstMismatch!.notes.some(n => n.includes('missing'))).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('reports first mismatch in time order even if later mismatches exist', () => {
|
|
442
|
+
const provider: SourceBars = {
|
|
443
|
+
source: 'provider',
|
|
444
|
+
bars: [
|
|
445
|
+
{ time: 1_000_000, open: 100, high: 200, low: 90, close: 150 }, // high differs
|
|
446
|
+
{ time: 1_000_060, open: 105, high: 300, low: 100, close: 200 }, // high also differs
|
|
447
|
+
],
|
|
448
|
+
};
|
|
449
|
+
const db: SourceBars = {
|
|
450
|
+
source: 'database',
|
|
451
|
+
bars: [
|
|
452
|
+
{ time: 1_000_000, open: 100, high: 110, low: 90, close: 150 },
|
|
453
|
+
{ time: 1_000_060, open: 105, high: 115, low: 100, close: 200 },
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
const report = compareSources(provider, db, '1m', 'BTC', 1_000_000, 1_000_120);
|
|
457
|
+
expect(report.firstMismatch!.time).toBe(1_000_000); // oldest first
|
|
458
|
+
expect(report.totalMismatches).toBe(2);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('checkAllSources', () => {
|
|
463
|
+
it('produces N*(N-1)/2 pairwise reports', () => {
|
|
464
|
+
const sources = [
|
|
465
|
+
{ source: 'provider' as const, bars: [] },
|
|
466
|
+
{ source: 'database' as const, bars: [] },
|
|
467
|
+
{ source: 'sdk' as const, bars: [] },
|
|
468
|
+
];
|
|
469
|
+
const reports = checkAllSources(sources, 'BTC', '1h', 0, 100_000);
|
|
470
|
+
expect(reports.length).toBe(3); // C(3,2) = 3
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe('formatReport', () => {
|
|
475
|
+
it('includes IDENTICAL when no divergence', () => {
|
|
476
|
+
const bars: SourceBars = {
|
|
477
|
+
source: 'provider',
|
|
478
|
+
bars: [{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 }],
|
|
479
|
+
};
|
|
480
|
+
const report = compareSources(bars, { source: 'database', bars: bars.bars }, '1m', 'BTC', 1_000_000, 1_000_060);
|
|
481
|
+
const text = formatReport(report);
|
|
482
|
+
expect(text).toContain('IDENTICAL');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('includes DIVERGED and mismatch details when sources differ', () => {
|
|
486
|
+
const provider: SourceBars = {
|
|
487
|
+
source: 'provider',
|
|
488
|
+
bars: [{ time: 1_000_000, open: 100, high: 999, low: 90, close: 105 }],
|
|
489
|
+
};
|
|
490
|
+
const db: SourceBars = {
|
|
491
|
+
source: 'database',
|
|
492
|
+
bars: [{ time: 1_000_000, open: 100, high: 110, low: 90, close: 105 }],
|
|
493
|
+
};
|
|
494
|
+
const report = compareSources(provider, db, '1m', 'BTC', 1_000_000, 1_000_060);
|
|
495
|
+
const text = formatReport(report);
|
|
496
|
+
expect(text).toContain('DIVERGED');
|
|
497
|
+
expect(text).toContain('1000000'); // time
|
|
498
|
+
expect(text).toContain('high');
|
|
499
|
+
});
|
|
500
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Public API Surface — type-level integration test.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that @forgecharts/sdk exports the required public symbols.
|
|
5
|
+
* Compiled by `tsc --noEmit` (no runtime execution needed).
|
|
6
|
+
*
|
|
7
|
+
* The three primary integration points are:
|
|
8
|
+
* 1. createChart() — vanilla JS/TS DOM-based embed
|
|
9
|
+
* 2. ChartCanvas — React component embed
|
|
10
|
+
* 3. ChartWorkspace — full-workspace React embed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
// Primary chart config
|
|
15
|
+
ChartConfig,
|
|
16
|
+
ChartInterval,
|
|
17
|
+
ChartTheme,
|
|
18
|
+
ChartEventMap,
|
|
19
|
+
ChartLayout,
|
|
20
|
+
// Datafeed contract
|
|
21
|
+
IDatafeed,
|
|
22
|
+
HistoricalBarsResult,
|
|
23
|
+
SymbolInfo,
|
|
24
|
+
// Trading
|
|
25
|
+
ChartOrder,
|
|
26
|
+
ChartPosition,
|
|
27
|
+
ExecutionFill,
|
|
28
|
+
// Capabilities
|
|
29
|
+
ChartCapabilities,
|
|
30
|
+
// Indicator
|
|
31
|
+
IndicatorConfig,
|
|
32
|
+
IndicatorType,
|
|
33
|
+
} from '@forgecharts/sdk';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
createChart,
|
|
37
|
+
TChart,
|
|
38
|
+
DatafeedConnector,
|
|
39
|
+
TradingOverlayStore,
|
|
40
|
+
LicenseManager,
|
|
41
|
+
CandleEngine,
|
|
42
|
+
ReferenceAPI,
|
|
43
|
+
// Capability helpers
|
|
44
|
+
getCapabilities,
|
|
45
|
+
isManagedMode,
|
|
46
|
+
isUnmanagedMode,
|
|
47
|
+
} from '@forgecharts/sdk';
|
|
48
|
+
|
|
49
|
+
// ── Engine internals must NOT be importable from the root path ────────────
|
|
50
|
+
// The following would correctly fail to compile if enabled:
|
|
51
|
+
// import { Chart } from '@forgecharts/sdk'; // ❌ moved to ./internal
|
|
52
|
+
// import { PixiChart } from '@forgecharts/sdk'; // ❌ moved to ./internal
|
|
53
|
+
|
|
54
|
+
// ── Engine internals ARE importable from the internal path ────────────────
|
|
55
|
+
import { Chart, PixiChart, InteractionManager } from '@forgecharts/sdk/internal';
|
|
56
|
+
|
|
57
|
+
// ── Type assertions ───────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
// createChart must be callable and TChart must be exported
|
|
60
|
+
void createChart;
|
|
61
|
+
void TChart;
|
|
62
|
+
|
|
63
|
+
// DatafeedConnector, TradingOverlayStore, and capability helpers must be exported
|
|
64
|
+
void DatafeedConnector;
|
|
65
|
+
void TradingOverlayStore;
|
|
66
|
+
void getCapabilities;
|
|
67
|
+
|
|
68
|
+
// Internal engine: Chart, PixiChart, InteractionManager must still be available
|
|
69
|
+
void Chart;
|
|
70
|
+
void PixiChart;
|
|
71
|
+
void InteractionManager;
|
|
72
|
+
void LicenseManager;
|
|
73
|
+
void CandleEngine;
|
|
74
|
+
void ReferenceAPI;
|
|
75
|
+
void isManagedMode;
|
|
76
|
+
void isUnmanagedMode;
|