@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,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChartRuntimeResolver — unit tests
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - No license → unmanaged, all managed UI hidden, overlays/indicators/drawings visible
|
|
6
|
+
* - Unmanaged license → same as no license for UI gating
|
|
7
|
+
* - Managed license → all managed UI visible, external ingestion blocked
|
|
8
|
+
* - Feature flags override defaults within a mode
|
|
9
|
+
* - getCapabilities() snapshot is consistent with individual methods
|
|
10
|
+
* - Module-level convenience functions alias correctly
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
14
|
+
import { LicenseManager } from '../LicenseManager';
|
|
15
|
+
import { ChartRuntimeResolver } from '../ChartRuntimeResolver';
|
|
16
|
+
import type { LicensePayload } from '../licenseTypes';
|
|
17
|
+
|
|
18
|
+
function setLicense(payload: LicensePayload | null) {
|
|
19
|
+
const lm = LicenseManager.getInstance();
|
|
20
|
+
if (payload === null) { lm.clear(); } else { lm.loadLicense(payload); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const UNMANAGED_PAYLOAD: LicensePayload = { licenseKey: 'u1', mode: 'unmanaged' };
|
|
24
|
+
const MANAGED_PAYLOAD: LicensePayload = { licenseKey: 'm1', mode: 'managed' };
|
|
25
|
+
|
|
26
|
+
describe('ChartRuntimeResolver — no license (safe defaults)', () => {
|
|
27
|
+
beforeEach(() => setLicense(null));
|
|
28
|
+
|
|
29
|
+
it('isUnmanagedMode() is true', () => {
|
|
30
|
+
expect(ChartRuntimeResolver.getInstance().isUnmanagedMode()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('isManagedMode() is false', () => {
|
|
34
|
+
expect(ChartRuntimeResolver.getInstance().isManagedMode()).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('managed UI render flags are all false', () => {
|
|
38
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
39
|
+
expect(r.canRenderOrderEntry()).toBe(false);
|
|
40
|
+
expect(r.canRenderBuySellButtons()).toBe(false);
|
|
41
|
+
expect(r.canRenderOrderTicket()).toBe(false);
|
|
42
|
+
expect(r.canRenderBracketControls()).toBe(false);
|
|
43
|
+
expect(r.canRenderOrderModificationControls()).toBe(false);
|
|
44
|
+
expect(r.canRenderManagedTradingControls()).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('canRenderExternalOverlayOnlyMode() is true', () => {
|
|
48
|
+
expect(ChartRuntimeResolver.getInstance().canRenderExternalOverlayOnlyMode()).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('always-on render flags remain true', () => {
|
|
52
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
53
|
+
expect(r.canRenderCandles()).toBe(true);
|
|
54
|
+
expect(r.canRenderIndicators()).toBe(true);
|
|
55
|
+
expect(r.canRenderDrawings()).toBe(true);
|
|
56
|
+
expect(r.canRenderPositions()).toBe(true);
|
|
57
|
+
expect(r.canRenderFills()).toBe(true);
|
|
58
|
+
expect(r.canRenderOverlays()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('external ingestion is allowed in unmanaged mode', () => {
|
|
62
|
+
expect(ChartRuntimeResolver.getInstance().canUseExternalIngestion()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('ChartRuntimeResolver — explicit unmanaged license', () => {
|
|
67
|
+
beforeEach(() => setLicense(UNMANAGED_PAYLOAD));
|
|
68
|
+
|
|
69
|
+
it('matches the no-license behaviour for all managed flags', () => {
|
|
70
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
71
|
+
expect(r.canRenderOrderEntry()).toBe(false);
|
|
72
|
+
expect(r.canRenderManagedTradingControls()).toBe(false);
|
|
73
|
+
expect(r.canRenderExternalOverlayOnlyMode()).toBe(true);
|
|
74
|
+
expect(r.canRenderCandles()).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('ChartRuntimeResolver — managed license (full features)', () => {
|
|
79
|
+
beforeEach(() => setLicense(MANAGED_PAYLOAD));
|
|
80
|
+
|
|
81
|
+
it('isManagedMode() is true', () => {
|
|
82
|
+
expect(ChartRuntimeResolver.getInstance().isManagedMode()).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('external ingestion is blocked in managed mode', () => {
|
|
86
|
+
expect(ChartRuntimeResolver.getInstance().canUseExternalIngestion()).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('managed UI render flags are all true (no feature restrictions)', () => {
|
|
90
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
91
|
+
expect(r.canRenderOrderEntry()).toBe(true);
|
|
92
|
+
expect(r.canRenderBuySellButtons()).toBe(true);
|
|
93
|
+
expect(r.canRenderOrderTicket()).toBe(true);
|
|
94
|
+
expect(r.canRenderBracketControls()).toBe(true);
|
|
95
|
+
expect(r.canRenderOrderModificationControls()).toBe(true);
|
|
96
|
+
expect(r.canRenderManagedTradingControls()).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('canRenderExternalOverlayOnlyMode() is false', () => {
|
|
100
|
+
expect(ChartRuntimeResolver.getInstance().canRenderExternalOverlayOnlyMode()).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('always-on flags still true in managed mode', () => {
|
|
104
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
105
|
+
expect(r.canRenderCandles()).toBe(true);
|
|
106
|
+
expect(r.canRenderPositions()).toBe(true);
|
|
107
|
+
expect(r.canRenderFills()).toBe(true);
|
|
108
|
+
expect(r.canRenderDrawings()).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('ChartRuntimeResolver — feature flags restrict managed capabilities', () => {
|
|
113
|
+
it('orderEntry: false disables order entry render flags', () => {
|
|
114
|
+
setLicense({ licenseKey: 'mf', mode: 'managed', features: { orderEntry: false } });
|
|
115
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
116
|
+
expect(r.canRenderOrderEntry()).toBe(false);
|
|
117
|
+
expect(r.canRenderBuySellButtons()).toBe(false);
|
|
118
|
+
expect(r.canRenderOrderTicket()).toBe(false);
|
|
119
|
+
// bracket/drag also depend on order entry
|
|
120
|
+
expect(r.canRenderBracketControls()).toBe(false);
|
|
121
|
+
expect(r.canRenderOrderModificationControls()).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('bracketOrders: false hides bracket controls but not basic order entry', () => {
|
|
125
|
+
setLicense({
|
|
126
|
+
licenseKey: 'mb',
|
|
127
|
+
mode: 'managed',
|
|
128
|
+
features: { orderEntry: true, bracketOrders: false },
|
|
129
|
+
});
|
|
130
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
131
|
+
expect(r.canRenderOrderEntry()).toBe(true);
|
|
132
|
+
expect(r.canRenderBracketControls()).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('draggableOrders: false hides modification controls', () => {
|
|
136
|
+
setLicense({
|
|
137
|
+
licenseKey: 'md',
|
|
138
|
+
mode: 'managed',
|
|
139
|
+
features: { orderEntry: true, draggableOrders: false },
|
|
140
|
+
});
|
|
141
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
142
|
+
expect(r.canRenderOrderEntry()).toBe(true);
|
|
143
|
+
expect(r.canRenderOrderModificationControls()).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('indicators: false disables indicator rendering in both modes', () => {
|
|
147
|
+
setLicense({ licenseKey: 'ui', mode: 'unmanaged', features: { indicators: false } });
|
|
148
|
+
expect(ChartRuntimeResolver.getInstance().canRenderIndicators()).toBe(false);
|
|
149
|
+
|
|
150
|
+
setLicense({ licenseKey: 'mi', mode: 'managed', features: { indicators: false } });
|
|
151
|
+
expect(ChartRuntimeResolver.getInstance().canRenderIndicators()).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('ChartRuntimeResolver — getCapabilities() snapshot', () => {
|
|
156
|
+
it('snapshot is consistent with individual method values (unmanaged)', () => {
|
|
157
|
+
setLicense(UNMANAGED_PAYLOAD);
|
|
158
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
159
|
+
const caps = r.getCapabilities();
|
|
160
|
+
|
|
161
|
+
expect(caps.mode).toBe('unmanaged');
|
|
162
|
+
expect(caps.renderOrderEntry).toBe(r.canRenderOrderEntry());
|
|
163
|
+
expect(caps.renderBuySellButtons).toBe(r.canRenderBuySellButtons());
|
|
164
|
+
expect(caps.renderBracketControls).toBe(r.canRenderBracketControls());
|
|
165
|
+
expect(caps.renderManagedTradingControls).toBe(r.canRenderManagedTradingControls());
|
|
166
|
+
expect(caps.renderExternalOverlayOnly).toBe(r.canRenderExternalOverlayOnlyMode());
|
|
167
|
+
expect(caps.renderCandles).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('snapshot is consistent with individual method values (managed)', () => {
|
|
171
|
+
setLicense(MANAGED_PAYLOAD);
|
|
172
|
+
const r = ChartRuntimeResolver.getInstance();
|
|
173
|
+
const caps = r.getCapabilities();
|
|
174
|
+
|
|
175
|
+
expect(caps.mode).toBe('managed');
|
|
176
|
+
expect(caps.renderOrderEntry).toBe(r.canRenderOrderEntry());
|
|
177
|
+
expect(caps.renderBracketControls).toBe(r.canRenderBracketControls());
|
|
178
|
+
expect(caps.renderExternalOverlayOnly).toBe(false);
|
|
179
|
+
expect(caps.renderCandles).toBe(true);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('ChartRuntimeResolver — module-level convenience exports', () => {
|
|
184
|
+
it('module-level functions reflect the singleton state', async () => {
|
|
185
|
+
const {
|
|
186
|
+
isManagedMode, isUnmanagedMode,
|
|
187
|
+
canRenderOrderEntry, canRenderManagedTradingControls,
|
|
188
|
+
canRenderExternalOverlayOnlyMode, canRenderCandles,
|
|
189
|
+
getCapabilities,
|
|
190
|
+
} = await import('../ChartRuntimeResolver');
|
|
191
|
+
|
|
192
|
+
setLicense(null);
|
|
193
|
+
expect(isUnmanagedMode()).toBe(true);
|
|
194
|
+
expect(isManagedMode()).toBe(false);
|
|
195
|
+
expect(canRenderOrderEntry()).toBe(false);
|
|
196
|
+
expect(canRenderManagedTradingControls()).toBe(false);
|
|
197
|
+
expect(canRenderExternalOverlayOnlyMode()).toBe(true);
|
|
198
|
+
expect(canRenderCandles()).toBe(true);
|
|
199
|
+
expect(getCapabilities().mode).toBe('unmanaged');
|
|
200
|
+
|
|
201
|
+
setLicense(MANAGED_PAYLOAD);
|
|
202
|
+
expect(isManagedMode()).toBe(true);
|
|
203
|
+
expect(canRenderOrderEntry()).toBe(true);
|
|
204
|
+
expect(canRenderManagedTradingControls()).toBe(true);
|
|
205
|
+
expect(canRenderExternalOverlayOnlyMode()).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LicenseManager — unit tests
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Default mode is 'unmanaged' with no license installed
|
|
6
|
+
* - loadLicense drives mode and features correctly
|
|
7
|
+
* - validateLicense resolves a payload (placeholder logic)
|
|
8
|
+
* - clear() resets to safe unmanaged defaults
|
|
9
|
+
* - subscribe/unsubscribe fires on every mutation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
13
|
+
import { LicenseManager } from '../LicenseManager';
|
|
14
|
+
import type { LicensePayload } from '../licenseTypes';
|
|
15
|
+
|
|
16
|
+
const FAKE_URL = 'http://localhost:4100/api/public/license/verify';
|
|
17
|
+
|
|
18
|
+
// Stub fetch so validateLicense tests don't need a real server
|
|
19
|
+
function stubFetch(response: object): void {
|
|
20
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
|
21
|
+
ok: true,
|
|
22
|
+
json: async () => response,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Always work with a fresh singleton state between tests
|
|
27
|
+
function reset() {
|
|
28
|
+
LicenseManager.getInstance().clear();
|
|
29
|
+
vi.unstubAllGlobals();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('LicenseManager — defaults', () => {
|
|
33
|
+
beforeEach(reset);
|
|
34
|
+
|
|
35
|
+
it('default mode is unmanaged when no license installed', () => {
|
|
36
|
+
expect(LicenseManager.getInstance().getMode()).toBe('unmanaged');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('getLicense() returns null before any license is loaded', () => {
|
|
40
|
+
expect(LicenseManager.getInstance().getLicense()).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('getFeatures() returns an empty object by default', () => {
|
|
44
|
+
expect(LicenseManager.getInstance().getFeatures()).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('hasFeature() returns false for any key when no license', () => {
|
|
48
|
+
const lm = LicenseManager.getInstance();
|
|
49
|
+
expect(lm.hasFeature('orderEntry')).toBe(false);
|
|
50
|
+
expect(lm.hasFeature('bracketOrders')).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('LicenseManager — loadLicense', () => {
|
|
55
|
+
beforeEach(reset);
|
|
56
|
+
|
|
57
|
+
it('sets mode to managed when payload.mode is managed', () => {
|
|
58
|
+
const lm = LicenseManager.getInstance();
|
|
59
|
+
const payload: LicensePayload = { licenseKey: 'k1', mode: 'managed' };
|
|
60
|
+
lm.loadLicense(payload);
|
|
61
|
+
expect(lm.getMode()).toBe('managed');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('sets mode to unmanaged when payload.mode is unmanaged', () => {
|
|
65
|
+
const lm = LicenseManager.getInstance();
|
|
66
|
+
const payload: LicensePayload = { licenseKey: 'k2', mode: 'unmanaged' };
|
|
67
|
+
lm.loadLicense(payload);
|
|
68
|
+
expect(lm.getMode()).toBe('unmanaged');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('exposes feature flags from the payload', () => {
|
|
72
|
+
const lm = LicenseManager.getInstance();
|
|
73
|
+
lm.loadLicense({
|
|
74
|
+
licenseKey: 'k3',
|
|
75
|
+
mode: 'managed',
|
|
76
|
+
features: { orderEntry: true, bracketOrders: true, draggableOrders: false },
|
|
77
|
+
});
|
|
78
|
+
expect(lm.hasFeature('orderEntry')).toBe(true);
|
|
79
|
+
expect(lm.hasFeature('bracketOrders')).toBe(true);
|
|
80
|
+
expect(lm.hasFeature('draggableOrders')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('getLicense() returns the loaded payload', () => {
|
|
84
|
+
const lm = LicenseManager.getInstance();
|
|
85
|
+
const payload: LicensePayload = { licenseKey: 'k4', mode: 'managed' };
|
|
86
|
+
lm.loadLicense(payload);
|
|
87
|
+
expect(lm.getLicense()).toEqual(payload);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('LicenseManager — validateLicense', () => {
|
|
92
|
+
beforeEach(reset);
|
|
93
|
+
|
|
94
|
+
it('resolves a LicensePayload for a non-empty key', async () => {
|
|
95
|
+
stubFetch({ valid: true, licenseKey: 'TEST-KEY-XYZ', mode: 'unmanaged', plan: 'default', expiresAt: null, features: {} });
|
|
96
|
+
const lm = LicenseManager.getInstance();
|
|
97
|
+
const payload = await lm.validateLicense('test-key-xyz', FAKE_URL);
|
|
98
|
+
expect(payload).toHaveProperty('licenseKey');
|
|
99
|
+
expect(payload).toHaveProperty('mode');
|
|
100
|
+
expect(lm.getLicense()).not.toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects when the key is empty', async () => {
|
|
104
|
+
const lm = LicenseManager.getInstance();
|
|
105
|
+
await expect(lm.validateLicense('', FAKE_URL)).rejects.toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('rejects when the key is whitespace-only', async () => {
|
|
109
|
+
const lm = LicenseManager.getInstance();
|
|
110
|
+
await expect(lm.validateLicense(' ', FAKE_URL)).rejects.toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('LicenseManager — clear', () => {
|
|
115
|
+
beforeEach(reset);
|
|
116
|
+
|
|
117
|
+
it('resets mode to unmanaged after a managed license', () => {
|
|
118
|
+
const lm = LicenseManager.getInstance();
|
|
119
|
+
lm.loadLicense({ licenseKey: 'k', mode: 'managed' });
|
|
120
|
+
expect(lm.getMode()).toBe('managed');
|
|
121
|
+
lm.clear();
|
|
122
|
+
expect(lm.getMode()).toBe('unmanaged');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('nulls out getLicense() after clear', () => {
|
|
126
|
+
const lm = LicenseManager.getInstance();
|
|
127
|
+
lm.loadLicense({ licenseKey: 'k', mode: 'managed' });
|
|
128
|
+
lm.clear();
|
|
129
|
+
expect(lm.getLicense()).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('LicenseManager — subscribe / notify', () => {
|
|
134
|
+
beforeEach(reset);
|
|
135
|
+
|
|
136
|
+
it('subscriber is called when loadLicense is invoked', () => {
|
|
137
|
+
const lm = LicenseManager.getInstance();
|
|
138
|
+
const spy = vi.fn();
|
|
139
|
+
lm.subscribe(spy);
|
|
140
|
+
lm.loadLicense({ licenseKey: 'k', mode: 'unmanaged' });
|
|
141
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('subscriber is called when clear() is invoked', () => {
|
|
145
|
+
const lm = LicenseManager.getInstance();
|
|
146
|
+
const spy = vi.fn();
|
|
147
|
+
lm.subscribe(spy);
|
|
148
|
+
lm.clear();
|
|
149
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('subscriber is called when validateLicense resolves', async () => {
|
|
153
|
+
stubFetch({ valid: true, licenseKey: 'KEY', mode: 'unmanaged', plan: 'default', expiresAt: null, features: {} });
|
|
154
|
+
const lm = LicenseManager.getInstance();
|
|
155
|
+
const spy = vi.fn();
|
|
156
|
+
lm.subscribe(spy);
|
|
157
|
+
await lm.validateLicense('key', FAKE_URL);
|
|
158
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('unsubscribe stops future notifications', () => {
|
|
162
|
+
const lm = LicenseManager.getInstance();
|
|
163
|
+
const spy = vi.fn();
|
|
164
|
+
const unsub = lm.subscribe(spy);
|
|
165
|
+
unsub();
|
|
166
|
+
lm.loadLicense({ licenseKey: 'k', mode: 'unmanaged' });
|
|
167
|
+
expect(spy).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('multiple subscribers each fire independently', () => {
|
|
171
|
+
const lm = LicenseManager.getInstance();
|
|
172
|
+
const spyA = vi.fn();
|
|
173
|
+
const spyB = vi.fn();
|
|
174
|
+
lm.subscribe(spyA);
|
|
175
|
+
lm.subscribe(spyB);
|
|
176
|
+
lm.loadLicense({ licenseKey: 'k', mode: 'unmanaged' });
|
|
177
|
+
expect(spyA).toHaveBeenCalledTimes(1);
|
|
178
|
+
expect(spyB).toHaveBeenCalledTimes(1);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type LicenseMode = 'managed' | 'unmanaged';
|
|
2
|
+
|
|
3
|
+
export interface LicenseFeatures {
|
|
4
|
+
orderEntry?: boolean;
|
|
5
|
+
overlays?: boolean;
|
|
6
|
+
indicators?: boolean;
|
|
7
|
+
managedTrading?: boolean;
|
|
8
|
+
unmanagedIngestion?: boolean;
|
|
9
|
+
draggableOrders?: boolean;
|
|
10
|
+
bracketOrders?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LicensePayload {
|
|
14
|
+
licenseKey: string;
|
|
15
|
+
mode: LicenseMode;
|
|
16
|
+
plan?: string;
|
|
17
|
+
expires?: string;
|
|
18
|
+
features?: LicenseFeatures;
|
|
19
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PineCompiler — public entry point for the Pine Script compatibility layer.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline:
|
|
5
|
+
* Pine source → PineLexer → PineToken[] → PineParser → PineProgram
|
|
6
|
+
* → PineTranspiler → TScript source → TScriptIndicator runtime
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PineParser } from './pine-parser';
|
|
10
|
+
import { PineTranspiler } from './pine-transpiler';
|
|
11
|
+
import { DiagnosticBag } from './diagnostics';
|
|
12
|
+
import type { Diagnostic } from './diagnostics';
|
|
13
|
+
import { TScriptIndicator } from '../tscript/TScriptIndicator';
|
|
14
|
+
import type { OHLCV } from '@forgecharts/types';
|
|
15
|
+
import type { IndicatorPoint } from '../core/IndicatorEngine';
|
|
16
|
+
|
|
17
|
+
export type { Diagnostic };
|
|
18
|
+
export type { DiagnosticSeverity } from './diagnostics';
|
|
19
|
+
|
|
20
|
+
export interface PineCompileResult {
|
|
21
|
+
/** The transpiled TScript source string. */
|
|
22
|
+
readonly tscript: string;
|
|
23
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
24
|
+
/** `true` when there are no error-severity diagnostics. */
|
|
25
|
+
readonly ok: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class PineCompiler {
|
|
29
|
+
/**
|
|
30
|
+
* Compile Pine Script source to TScript.
|
|
31
|
+
* Parse errors throw a `SyntaxError`; semantic problems populate diagnostics.
|
|
32
|
+
*/
|
|
33
|
+
compile(pineSource: string): PineCompileResult {
|
|
34
|
+
const diag = new DiagnosticBag();
|
|
35
|
+
|
|
36
|
+
const parser = new PineParser(pineSource);
|
|
37
|
+
const program = parser.parse();
|
|
38
|
+
|
|
39
|
+
const transpiler = new PineTranspiler(diag);
|
|
40
|
+
const tscript = transpiler.transpile(program);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
tscript,
|
|
44
|
+
diagnostics: diag.diagnostics,
|
|
45
|
+
ok: !diag.hasErrors(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convenience: compile + run against a bar series.
|
|
51
|
+
* Returns the plot outputs and any diagnostics.
|
|
52
|
+
*/
|
|
53
|
+
static run(
|
|
54
|
+
pineSource: string,
|
|
55
|
+
bars: readonly OHLCV[],
|
|
56
|
+
): { plots: IndicatorPoint[][]; diagnostics: readonly Diagnostic[] } {
|
|
57
|
+
const compiler = new PineCompiler();
|
|
58
|
+
const result = compiler.compile(pineSource);
|
|
59
|
+
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
return { plots: [], diagnostics: result.diagnostics };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const indicator = new TScriptIndicator(result.tscript);
|
|
65
|
+
const plots = indicator.run(bars);
|
|
66
|
+
return { plots, diagnostics: result.diagnostics };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Diagnostic emitted by the Pine compiler pipeline. */
|
|
2
|
+
export type DiagnosticSeverity = 'error' | 'warning' | 'info';
|
|
3
|
+
|
|
4
|
+
export interface Diagnostic {
|
|
5
|
+
readonly severity: DiagnosticSeverity;
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly col: number;
|
|
9
|
+
/** Machine-readable code, e.g. 'PINE_UNSUPPORTED_FN' */
|
|
10
|
+
readonly code: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class DiagnosticBag {
|
|
14
|
+
private readonly _diags: Diagnostic[] = [];
|
|
15
|
+
|
|
16
|
+
add(
|
|
17
|
+
severity: DiagnosticSeverity,
|
|
18
|
+
message: string,
|
|
19
|
+
loc: { line: number; col: number },
|
|
20
|
+
code: string,
|
|
21
|
+
): void {
|
|
22
|
+
this._diags.push({ severity, message, line: loc.line, col: loc.col, code });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get diagnostics(): readonly Diagnostic[] { return this._diags; }
|
|
26
|
+
|
|
27
|
+
hasErrors(): boolean {
|
|
28
|
+
return this._diags.some(d => d.severity === 'error');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { PineParser } from './pine-parser';
|
|
2
|
+
export { PineTranspiler } from './pine-transpiler';
|
|
3
|
+
export { DiagnosticBag } from './diagnostics';
|
|
4
|
+
export { PineCompiler } from './PineCompiler';
|
|
5
|
+
export type { Diagnostic, DiagnosticSeverity } from './diagnostics';
|
|
6
|
+
export type { PineCompileResult } from './PineCompiler';
|
|
7
|
+
export type { PineProgram, PineStmt, PineExpr } from './pine-ast';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pine Script AST types — subset understood by the ForgeCharts Pine compat layer.
|
|
3
|
+
*
|
|
4
|
+
* Supported Pine v5 constructs:
|
|
5
|
+
* indicator(title, overlay?, ...)
|
|
6
|
+
* input(defval, title?, ...) / input.int() / input.float() / input.bool() / input.source()
|
|
7
|
+
* ta.sma() ta.ema() ta.wma() ta.rma() ta.rsi() ta.macd()
|
|
8
|
+
* ta.stdev() ta.highest() ta.lowest() ta.change() ta.mom() ta.atr()
|
|
9
|
+
* ta.crossover() ta.crossunder() ta.barssince()
|
|
10
|
+
* math.abs() math.max() math.min() math.round() math.floor() math.ceil()
|
|
11
|
+
* math.sqrt() math.log() math.pow() math.sign()
|
|
12
|
+
* plot() plotshape()
|
|
13
|
+
* nz() na()
|
|
14
|
+
* if / else blocks
|
|
15
|
+
* var declarations
|
|
16
|
+
* ternary a ? b : c
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ─── Locations ────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export type Loc = { line: number; col: number };
|
|
22
|
+
|
|
23
|
+
// ─── Literals ─────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export type PineNumberLit = { kind: 'PineNumberLit'; value: number; loc: Loc };
|
|
26
|
+
export type PineStringLit = { kind: 'PineStringLit'; value: string; loc: Loc };
|
|
27
|
+
export type PineBoolLit = { kind: 'PineBoolLit'; value: boolean; loc: Loc };
|
|
28
|
+
export type PineColorLit = { kind: 'PineColorLit'; value: string; loc: Loc }; // e.g. color.red
|
|
29
|
+
export type PineNa = { kind: 'PineNa'; loc: Loc };
|
|
30
|
+
|
|
31
|
+
// ─── Expressions ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export type PineIdent = {
|
|
34
|
+
kind: 'PineIdent';
|
|
35
|
+
name: string;
|
|
36
|
+
loc: Loc;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Namespaced call: ta.sma(src, len) or math.abs(x) */
|
|
40
|
+
export type PineNsCall = {
|
|
41
|
+
kind: 'PineNsCall';
|
|
42
|
+
namespace: string; // 'ta' | 'math' | 'input' | 'color' | etc.
|
|
43
|
+
fn: string;
|
|
44
|
+
args: PineExpr[];
|
|
45
|
+
namedArgs: Map<string, PineExpr>;
|
|
46
|
+
loc: Loc;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Plain function call: sma(src, len), indicator(...), plot(...) */
|
|
50
|
+
export type PineCall = {
|
|
51
|
+
kind: 'PineCall';
|
|
52
|
+
fn: string;
|
|
53
|
+
args: PineExpr[];
|
|
54
|
+
namedArgs: Map<string, PineExpr>;
|
|
55
|
+
loc: Loc;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type PineIndex = {
|
|
59
|
+
kind: 'PineIndex';
|
|
60
|
+
series: PineExpr;
|
|
61
|
+
index: PineExpr;
|
|
62
|
+
loc: Loc;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type PineBinary = {
|
|
66
|
+
kind: 'PineBinary';
|
|
67
|
+
op: '+' | '-' | '*' | '/' | '%' | '<' | '>' | '<=' | '>=' | '==' | '!=' | 'and' | 'or';
|
|
68
|
+
left: PineExpr;
|
|
69
|
+
right: PineExpr;
|
|
70
|
+
loc: Loc;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type PineUnary = {
|
|
74
|
+
kind: 'PineUnary';
|
|
75
|
+
op: '-' | 'not';
|
|
76
|
+
operand: PineExpr;
|
|
77
|
+
loc: Loc;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export type PineTernary = {
|
|
81
|
+
kind: 'PineTernary';
|
|
82
|
+
condition: PineExpr;
|
|
83
|
+
consequent: PineExpr;
|
|
84
|
+
alternate: PineExpr;
|
|
85
|
+
loc: Loc;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Member access: color.red, bar_index, etc. — resolved at transpile time */
|
|
89
|
+
export type PineMember = {
|
|
90
|
+
kind: 'PineMember';
|
|
91
|
+
object: string;
|
|
92
|
+
prop: string;
|
|
93
|
+
loc: Loc;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type PineExpr =
|
|
97
|
+
| PineNumberLit
|
|
98
|
+
| PineStringLit
|
|
99
|
+
| PineBoolLit
|
|
100
|
+
| PineColorLit
|
|
101
|
+
| PineNa
|
|
102
|
+
| PineIdent
|
|
103
|
+
| PineNsCall
|
|
104
|
+
| PineCall
|
|
105
|
+
| PineIndex
|
|
106
|
+
| PineBinary
|
|
107
|
+
| PineUnary
|
|
108
|
+
| PineTernary
|
|
109
|
+
| PineMember;
|
|
110
|
+
|
|
111
|
+
// ─── Statements ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export type PineIndicatorDecl = {
|
|
114
|
+
kind: 'PineIndicatorDecl';
|
|
115
|
+
args: PineExpr[];
|
|
116
|
+
namedArgs: Map<string, PineExpr>;
|
|
117
|
+
loc: Loc;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** var / varip declaration */
|
|
121
|
+
export type PineVarDecl = {
|
|
122
|
+
kind: 'PineVarDecl';
|
|
123
|
+
modifier: 'var' | 'varip';
|
|
124
|
+
name: string;
|
|
125
|
+
typeHint?: string;
|
|
126
|
+
value: PineExpr;
|
|
127
|
+
loc: Loc;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type PineAssign = {
|
|
131
|
+
kind: 'PineAssign';
|
|
132
|
+
name: string;
|
|
133
|
+
op: '=' | ':=';
|
|
134
|
+
value: PineExpr;
|
|
135
|
+
loc: Loc;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type PineExprStmt = {
|
|
139
|
+
kind: 'PineExprStmt';
|
|
140
|
+
expr: PineExpr;
|
|
141
|
+
loc: Loc;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type PineIf = {
|
|
145
|
+
kind: 'PineIf';
|
|
146
|
+
condition: PineExpr;
|
|
147
|
+
then: PineStmt[];
|
|
148
|
+
else_: PineStmt[];
|
|
149
|
+
loc: Loc;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type PineStmt =
|
|
153
|
+
| PineIndicatorDecl
|
|
154
|
+
| PineVarDecl
|
|
155
|
+
| PineAssign
|
|
156
|
+
| PineExprStmt
|
|
157
|
+
| PineIf;
|
|
158
|
+
|
|
159
|
+
export type PineProgram = {
|
|
160
|
+
kind: 'PineProgram';
|
|
161
|
+
version: number; // extracted from //@version=5
|
|
162
|
+
stmts: PineStmt[];
|
|
163
|
+
};
|