@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,881 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ChartConfig,
|
|
3
|
+
ChartInterval,
|
|
4
|
+
ChartTheme,
|
|
5
|
+
ChartEventMap,
|
|
6
|
+
ChartLayout,
|
|
7
|
+
Drawing,
|
|
8
|
+
IDatafeed,
|
|
9
|
+
IndicatorConfig,
|
|
10
|
+
IndicatorInstance,
|
|
11
|
+
IPlugin,
|
|
12
|
+
IPublicChart,
|
|
13
|
+
OHLCV,
|
|
14
|
+
PaneDescriptor,
|
|
15
|
+
} from '@forgecharts/types';
|
|
16
|
+
import { Chart } from '../core/Chart';
|
|
17
|
+
import { EventBus } from './EventBus';
|
|
18
|
+
import { IndicatorRegistry } from './IndicatorRegistry';
|
|
19
|
+
import { DrawingManager } from './DrawingManager';
|
|
20
|
+
import { LayoutManager } from './LayoutManager';
|
|
21
|
+
import { PaneManager } from './PaneManager';
|
|
22
|
+
import { DatafeedConnector } from '../datafeed/DatafeedConnector';
|
|
23
|
+
import type { ISeries } from '../types/ISeries';
|
|
24
|
+
import type { CoordTransform } from '../core/CoordTransform';
|
|
25
|
+
import { TradingOverlayStore } from '../trading/TradingOverlayStore';
|
|
26
|
+
import { UnmanagedIngestion } from '../trading/UnmanagedIngestion';
|
|
27
|
+
import { isUnmanagedMode } from '../licensing/ChartRuntimeResolver';
|
|
28
|
+
import type { CandleInput, ChartOrder, ChartPosition, ExecutionFill } from '../trading/tradingTypes';
|
|
29
|
+
import { ManagedTradingController } from '../trading/managed/ManagedTradingController';
|
|
30
|
+
|
|
31
|
+
/** Candle duration in seconds per timeframe — used for the history-prefetch threshold. */
|
|
32
|
+
const _TF_SECS: Partial<Record<string, number>> = {
|
|
33
|
+
'1s': 1, '5s': 5, '10s': 10, '30s': 30,
|
|
34
|
+
'1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800,
|
|
35
|
+
'1h': 3600, '2h': 7200, '4h': 14400, '6h': 21600, '12h': 43200,
|
|
36
|
+
'1d': 86400, '3d': 259200, '1w': 604800, '1M': 2592000,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* TChart — the stable public chart instance returned by `createChart()`.
|
|
41
|
+
*
|
|
42
|
+
* Wraps the internal canvas `Chart` engine and exposes the documented
|
|
43
|
+
* public API. Application code and plugins should only interact with
|
|
44
|
+
* `TChart`/`IPublicChart`; never with the internal `Chart` class directly.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { createChart } from '@forgecharts/sdk';
|
|
49
|
+
*
|
|
50
|
+
* const chart = createChart({
|
|
51
|
+
* container: document.getElementById('chart')!,
|
|
52
|
+
* symbol: 'BTCUSDT',
|
|
53
|
+
* interval: '5m',
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* chart.on('symbolChanged', ({ symbol }) => console.log('Symbol:', symbol));
|
|
57
|
+
*
|
|
58
|
+
* chart.setTheme('light');
|
|
59
|
+
* const id = chart.addIndicator({ type: 'ema', params: { period: 20 } });
|
|
60
|
+
* chart.removeIndicator(id);
|
|
61
|
+
*
|
|
62
|
+
* const layout = chart.saveLayout(); // portable JSON-ready object
|
|
63
|
+
* chart.loadLayout(layout); // restore
|
|
64
|
+
*
|
|
65
|
+
* chart.destroy();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export class TChart implements IPublicChart {
|
|
69
|
+
private readonly _core: Chart;
|
|
70
|
+
private readonly _bus: EventBus;
|
|
71
|
+
private readonly _indicators: IndicatorRegistry;
|
|
72
|
+
private readonly _drawings: DrawingManager;
|
|
73
|
+
private readonly _panes: PaneManager;
|
|
74
|
+
private readonly _plugins: IPlugin[] = [];
|
|
75
|
+
|
|
76
|
+
/** Primary price series — created once; fed by the datafeed or directly. */
|
|
77
|
+
private readonly _series: ISeries;
|
|
78
|
+
/** Datafeed connector — null when no datafeed was provided. */
|
|
79
|
+
private _connector: DatafeedConnector | null = null;
|
|
80
|
+
|
|
81
|
+
private _symbol: string;
|
|
82
|
+
private _interval: ChartInterval;
|
|
83
|
+
private _theme: ChartTheme;
|
|
84
|
+
private _destroyed = false;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* In-memory auth token. Set via constructor `getAuthToken` callback or
|
|
88
|
+
* directly via `setAuthToken()`. Takes precedence over localStorage.
|
|
89
|
+
*/
|
|
90
|
+
private _authToken: string | null = null;
|
|
91
|
+
private readonly _getAuthToken: (() => string | Promise<string>) | null = null;
|
|
92
|
+
private readonly _apiUrl: string;
|
|
93
|
+
|
|
94
|
+
// ─ Shared trading overlay store (used by both unmanaged and managed modes) ──
|
|
95
|
+
private readonly _overlayStore: TradingOverlayStore = new TradingOverlayStore();
|
|
96
|
+
/** Lazy-initialised; only created when first unmanaged ingestion method is called. */
|
|
97
|
+
private _unmanagedIngestion: UnmanagedIngestion | null = null;
|
|
98
|
+
/** Lazy-initialised; only created when getManagedTrading() is first called. */
|
|
99
|
+
private _managedTrading: ManagedTradingController | null = null;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Returns a Bearer token for API calls. Resolution order:
|
|
103
|
+
* 1. In-memory `_authToken` (set via `setAuthToken()`).
|
|
104
|
+
* 2. `getAuthToken` callback from config (embedder-managed).
|
|
105
|
+
* 3. `localStorage["forgecharts-token"]` (built-in frontend fallback).
|
|
106
|
+
*
|
|
107
|
+
* Returns `null` when no token is available — API calls will be unauthenticated.
|
|
108
|
+
*/
|
|
109
|
+
async resolveAuthToken(): Promise<string | null> {
|
|
110
|
+
if (this._authToken) return this._authToken;
|
|
111
|
+
if (this._getAuthToken) {
|
|
112
|
+
try { return await this._getAuthToken(); } catch { return null; }
|
|
113
|
+
}
|
|
114
|
+
if (typeof localStorage !== 'undefined') {
|
|
115
|
+
return localStorage.getItem('forgecharts-token');
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
constructor(config: ChartConfig) {
|
|
121
|
+
this._symbol = config.symbol;
|
|
122
|
+
this._apiUrl = config.apiUrl ?? '';
|
|
123
|
+
if (config.getAuthToken) this._getAuthToken = config.getAuthToken;
|
|
124
|
+
this._interval = config.interval ?? '1h';
|
|
125
|
+
this._theme = config.theme ?? 'dark';
|
|
126
|
+
|
|
127
|
+
this._bus = new EventBus();
|
|
128
|
+
this._indicators = new IndicatorRegistry();
|
|
129
|
+
this._panes = new PaneManager();
|
|
130
|
+
|
|
131
|
+
this._core = new Chart(config.container, {
|
|
132
|
+
theme: this._theme,
|
|
133
|
+
handleScroll: !(config.disableScroll ?? false),
|
|
134
|
+
handleScale: !(config.disableZoom ?? false),
|
|
135
|
+
...(config.crosshair !== undefined ? { crosshair: config.crosshair } : {}),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Use the Chart's own DrawingManager so drawings are rendered automatically
|
|
139
|
+
this._drawings = this._core.drawingManager();
|
|
140
|
+
|
|
141
|
+
// Wire drawing callbacks
|
|
142
|
+
this._core.onDrawingCommitted = (drawing) => {
|
|
143
|
+
this._bus.emit('drawingCreated', drawing);
|
|
144
|
+
};
|
|
145
|
+
this._core.onDrawingDeleted = (id) => {
|
|
146
|
+
this._bus.emit('drawingRemoved', { id });
|
|
147
|
+
};
|
|
148
|
+
this._core.onDrawingContextMenu = (id, x, y) => {
|
|
149
|
+
this._bus.emit('drawingContextMenu', { id, x, y });
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Prime the chart with a default candlestick series and retain the reference
|
|
153
|
+
this._series = this._core.addSeries({ type: 'candlestick' });
|
|
154
|
+
|
|
155
|
+
// Tell the core chart which timeframe is active (for last-price countdown)
|
|
156
|
+
this._core.setInterval(this._interval);
|
|
157
|
+
|
|
158
|
+
// Wire datafeed if provided
|
|
159
|
+
if (config.datafeed !== undefined) {
|
|
160
|
+
this._connector = this._buildConnector(config.datafeed);
|
|
161
|
+
this._connector.connect(this._symbol, this._interval);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Lazy-load older history when the user pans/zooms the left edge into view.
|
|
165
|
+
// Fires on every pan/zoom; debounced internally by DatafeedConnector._isLoadingMore.
|
|
166
|
+
this._core.onViewportChanged = () => {
|
|
167
|
+
const connector = this._connector;
|
|
168
|
+
if (!connector) return;
|
|
169
|
+
const bars = this._series.data();
|
|
170
|
+
if (bars.length === 0) return;
|
|
171
|
+
const oldestBarTime = bars[0]!.time;
|
|
172
|
+
const { from } = this._core.getViewport().timeRange;
|
|
173
|
+
// Trigger when the viewport left edge is within 30 candles of the oldest loaded bar
|
|
174
|
+
const tfSec = _TF_SECS[this._interval] ?? 3600;
|
|
175
|
+
if (from < oldestBarTime + tfSec * 30) {
|
|
176
|
+
connector.loadMoreHistory(this._symbol, this._interval);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Attach plugins provided at construction time
|
|
181
|
+
for (const plugin of config.plugins ?? []) {
|
|
182
|
+
this._attachPlugin(plugin);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Getters ─────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/** Currently displayed symbol. */
|
|
189
|
+
get symbol(): string {
|
|
190
|
+
return this._symbol;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Currently active interval. */
|
|
194
|
+
get interval(): ChartInterval {
|
|
195
|
+
return this._interval;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Currently active theme. */
|
|
199
|
+
get theme(): ChartTheme {
|
|
200
|
+
return this._theme;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Data access ─────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/** Returns the current bar dataset (sorted ascending by time). */
|
|
206
|
+
getBars(): readonly OHLCV[] {
|
|
207
|
+
return this._series.data();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Returns the coordinate transform used for pixel ⇔ data conversions. */
|
|
211
|
+
getTransform(): CoordTransform {
|
|
212
|
+
return this._core.transform();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Returns the current sub-pane descriptors. */
|
|
216
|
+
getPanes(): readonly PaneDescriptor[] {
|
|
217
|
+
return this._panes.getPanes();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Returns all active indicator instances. */
|
|
221
|
+
getIndicators(): readonly IndicatorInstance[] {
|
|
222
|
+
return this._indicators.all();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Resizes a sub-pane by setting its height fraction.
|
|
227
|
+
* Remaining panes absorb the difference proportionally.
|
|
228
|
+
*/
|
|
229
|
+
resizePane(id: string, newFraction: number): void {
|
|
230
|
+
this._assertAlive();
|
|
231
|
+
this._panes.resizePane(id, newFraction);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─── Auth ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Sets a static Bearer token for API calls (layouts, etc.).
|
|
238
|
+
*
|
|
239
|
+
* Call this after your user authenticates. If you prefer token refresh,
|
|
240
|
+
* supply `getAuthToken` in `ChartConfig` instead — that callback is invoked
|
|
241
|
+
* on every API request so you can return a fresh token each time.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* // After your own login succeeds:
|
|
246
|
+
* const { forgechartsToken } = await myBackend.login(email, password);
|
|
247
|
+
* chart.setAuthToken(forgechartsToken);
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
setAuthToken(token: string): void {
|
|
251
|
+
this._authToken = token;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clears the in-memory auth token (e.g. on logout).
|
|
256
|
+
* After calling this the SDK falls back to `getAuthToken` or localStorage.
|
|
257
|
+
*/
|
|
258
|
+
clearAuthToken(): void {
|
|
259
|
+
this._authToken = null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── Setters ─────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Changes the displayed symbol.
|
|
266
|
+
* No-ops if the symbol is unchanged.
|
|
267
|
+
* Emits `symbolChanged`.
|
|
268
|
+
*/
|
|
269
|
+
setSymbol(symbol: string): void {
|
|
270
|
+
this._assertAlive();
|
|
271
|
+
if (symbol === this._symbol) return;
|
|
272
|
+
this._symbol = symbol;
|
|
273
|
+
this._bus.emit('symbolChanged', { symbol });
|
|
274
|
+
this._connector?.reconnect(symbol, this._interval);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Changes the active interval.
|
|
279
|
+
* No-ops if the interval is unchanged.
|
|
280
|
+
* Emits `intervalChanged`.
|
|
281
|
+
*/
|
|
282
|
+
setInterval(interval: ChartInterval): void {
|
|
283
|
+
this._assertAlive();
|
|
284
|
+
if (interval === this._interval) return;
|
|
285
|
+
this._interval = interval;
|
|
286
|
+
this._core.setInterval(interval);
|
|
287
|
+
this._bus.emit('intervalChanged', { interval });
|
|
288
|
+
this._connector?.reconnect(this._symbol, interval);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Switches the colour theme.
|
|
293
|
+
* No-ops if the theme is unchanged.
|
|
294
|
+
* Emits `themeChanged`.
|
|
295
|
+
*/
|
|
296
|
+
setTheme(theme: ChartTheme): void {
|
|
297
|
+
this._assertAlive();
|
|
298
|
+
if (theme === this._theme) return;
|
|
299
|
+
this._theme = theme;
|
|
300
|
+
this._core.applyOptions({ theme });
|
|
301
|
+
this._bus.emit('themeChanged', { theme });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Updates the timezone used for all time-axis labels. */
|
|
305
|
+
setTimezone(timezone: string): void {
|
|
306
|
+
this._assertAlive();
|
|
307
|
+
this._core.applyOptions({ timezone });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Applies low-level chart options (colors, theme, scales, etc.).
|
|
312
|
+
* Pass `{ colors: { background: '#fff' } }` to change individual canvas colors.
|
|
313
|
+
*/
|
|
314
|
+
applyOptions(options: Partial<import('@forgecharts/types').ChartOptions>): void {
|
|
315
|
+
this._assertAlive();
|
|
316
|
+
this._core.applyOptions(options);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Scrolls the viewport so the latest bar is visible at the right edge. */
|
|
320
|
+
scrollToEnd(): void {
|
|
321
|
+
this._assertAlive();
|
|
322
|
+
this._core.scrollToEnd();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Sets the default viewport: 10 days visible with the latest bar near the
|
|
327
|
+
* right edge (5% empty space on the right for future price action).
|
|
328
|
+
*/
|
|
329
|
+
fitDefaultView(): void {
|
|
330
|
+
this._assertAlive();
|
|
331
|
+
this._core.fitDefaultView();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Resets the price axis to auto-fit mode without changing the time range.
|
|
336
|
+
* Use this after restoring a saved viewport to prevent a stale priceRange
|
|
337
|
+
* from locking the scale at a wrong value (e.g. the default 0–100).
|
|
338
|
+
*/
|
|
339
|
+
resetPriceScale(): void {
|
|
340
|
+
this._assertAlive();
|
|
341
|
+
this._core.resetPriceScale();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Returns the current visible time and price ranges. */
|
|
345
|
+
getViewport(): import('@forgecharts/types').Viewport {
|
|
346
|
+
this._assertAlive();
|
|
347
|
+
return this._core.getViewport();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Restores a previously saved viewport (time + price ranges). */
|
|
351
|
+
restoreViewport(v: import('@forgecharts/types').Viewport): void {
|
|
352
|
+
this._assertAlive();
|
|
353
|
+
this._core.restoreViewport(v);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Indicators ──────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Registers an indicator and returns its opaque id.
|
|
360
|
+
* Emits `indicatorAdded`.
|
|
361
|
+
*
|
|
362
|
+
* @example
|
|
363
|
+
* ```ts
|
|
364
|
+
* const rsiId = chart.addIndicator({ type: 'rsi', params: { period: 14 } });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
addIndicator(config: IndicatorConfig): string {
|
|
368
|
+
this._assertAlive();
|
|
369
|
+
const id = this._indicators.add(config);
|
|
370
|
+
// Non-overlay indicators get their own dedicated sub-pane
|
|
371
|
+
if (config.overlay !== true) {
|
|
372
|
+
const label = _indicatorTypeLabel(config.type);
|
|
373
|
+
const paneId = this._panes.createPane(label);
|
|
374
|
+
this._panes.assignIndicator(id, paneId);
|
|
375
|
+
}
|
|
376
|
+
this._bus.emit('indicatorAdded', { id, config });
|
|
377
|
+
return id;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Removes a previously added indicator.
|
|
382
|
+
* Silently no-ops if the id is unknown.
|
|
383
|
+
* Emits `indicatorRemoved`.
|
|
384
|
+
*/
|
|
385
|
+
removeIndicator(id: string): void {
|
|
386
|
+
this._assertAlive();
|
|
387
|
+
// Find which pane holds this indicator before removal
|
|
388
|
+
const paneId = this._panes.getPanes().find((p) => p.indicatorIds.includes(id))?.id;
|
|
389
|
+
const removed = this._indicators.remove(id);
|
|
390
|
+
if (removed) {
|
|
391
|
+
this._panes.unassignIndicator(id);
|
|
392
|
+
// Remove the pane if it is now empty
|
|
393
|
+
if (paneId !== undefined) {
|
|
394
|
+
const pane = this._panes.getPanes().find((p) => p.id === paneId);
|
|
395
|
+
if (pane !== undefined && pane.indicatorIds.length === 0) {
|
|
396
|
+
this._panes.removePane(paneId);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this._bus.emit('indicatorRemoved', { id });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── Drawings ────────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Adds a drawing to the chart and returns its generated id.
|
|
407
|
+
* Emits `drawingCreated`.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* const id = chart.addDrawing({
|
|
412
|
+
* type: 'trendline',
|
|
413
|
+
* points: [
|
|
414
|
+
* { time: 1700000000, price: 45000 },
|
|
415
|
+
* { time: 1700003600, price: 46000 },
|
|
416
|
+
* ],
|
|
417
|
+
* });
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
addDrawing(drawing: Omit<Drawing, 'id'>): string {
|
|
421
|
+
this._assertAlive();
|
|
422
|
+
const id = this._drawings.add(drawing);
|
|
423
|
+
const full = this._drawings.get(id);
|
|
424
|
+
if (!full) throw new Error('[ForgeCharts] DrawingManager failed to store drawing');
|
|
425
|
+
this._bus.emit('drawingCreated', full);
|
|
426
|
+
this._core.markDirty();
|
|
427
|
+
return id;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Removes a single drawing by id.
|
|
432
|
+
* Emits `drawingRemoved`.
|
|
433
|
+
*/
|
|
434
|
+
removeDrawing(id: string): void {
|
|
435
|
+
this._assertAlive();
|
|
436
|
+
if (!this._drawings.has(id)) return;
|
|
437
|
+
this._drawings.remove(id);
|
|
438
|
+
this._bus.emit('drawingRemoved', { id });
|
|
439
|
+
this._core.markDirty();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Activates a drawing tool. Clicks will place anchors on the chart.
|
|
444
|
+
* The tool remains active after each commit (auto-repeat). Press Escape to cancel.
|
|
445
|
+
*/
|
|
446
|
+
startDrawingTool(type: import('@forgecharts/types').DrawingType): void {
|
|
447
|
+
this._assertAlive();
|
|
448
|
+
this._core.startDrawingTool(type);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Cancels an active drawing tool and returns to cursor mode. */
|
|
452
|
+
cancelDrawingTool(): void {
|
|
453
|
+
this._assertAlive();
|
|
454
|
+
this._core.cancelDrawingTool();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Show or hide the SDK crosshair lines.
|
|
459
|
+
* Call with `false` for Arrow mode; `true` for crosshair / dot / demonstration.
|
|
460
|
+
*/
|
|
461
|
+
setCrosshairEnabled(enabled: boolean): void {
|
|
462
|
+
this._assertAlive();
|
|
463
|
+
this._core.setCrosshairEnabled(enabled);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Hides (`true`) or restores (`false`) the native OS cursor for dot / demonstration pointer modes. */
|
|
467
|
+
setNativeCursorHidden(hidden: boolean): void {
|
|
468
|
+
this._assertAlive();
|
|
469
|
+
this._core.setNativeCursorHidden(hidden);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Deletes the currently selected drawing (if any). */
|
|
473
|
+
deleteSelectedDrawing(): void {
|
|
474
|
+
this._assertAlive();
|
|
475
|
+
this._core.deleteSelectedDrawing();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Shows or hides all drawings on the chart. */
|
|
479
|
+
setAllDrawingsVisible(v: boolean): void {
|
|
480
|
+
this._assertAlive();
|
|
481
|
+
this._core.setAllDrawingsVisible(v);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Shows or hides all indicators/series on the chart. */
|
|
485
|
+
setIndicatorsVisible(v: boolean): void {
|
|
486
|
+
this._assertAlive();
|
|
487
|
+
this._core.setIndicatorsVisible(v);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Removes all drawings from the chart.
|
|
492
|
+
* Emits `drawingsCleared`.
|
|
493
|
+
*/
|
|
494
|
+
clearDrawings(): void {
|
|
495
|
+
this._assertAlive();
|
|
496
|
+
this._drawings.clear();
|
|
497
|
+
this._bus.emit('drawingsCleared', {});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ─── Layout ──────────────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Restores a previously saved layout.
|
|
504
|
+
* Replaces current symbol, interval, theme, indicators, and drawings.
|
|
505
|
+
* Emits `layoutLoaded`.
|
|
506
|
+
*/
|
|
507
|
+
loadLayout(layout: ChartLayout): void {
|
|
508
|
+
this._assertAlive();
|
|
509
|
+
|
|
510
|
+
this._symbol = layout.symbol;
|
|
511
|
+
this._interval = layout.interval;
|
|
512
|
+
|
|
513
|
+
// Theme (use internal setter to propagate to canvas)
|
|
514
|
+
if (layout.theme !== this._theme) {
|
|
515
|
+
this._theme = layout.theme;
|
|
516
|
+
this._core.applyOptions({ theme: layout.theme });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Indicators
|
|
520
|
+
this._indicators.clear();
|
|
521
|
+
for (const inst of layout.indicators) {
|
|
522
|
+
this._indicators.add(inst.config);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Drawings — strip old ids and re-add (DrawingManager assigns new ids)
|
|
526
|
+
this._drawings.clear();
|
|
527
|
+
for (const { id: _discarded, ...rest } of layout.drawings) {
|
|
528
|
+
this._drawings.add(rest as Omit<Drawing, 'id'>);
|
|
529
|
+
}
|
|
530
|
+
this._core.markDirty();
|
|
531
|
+
|
|
532
|
+
// Panes
|
|
533
|
+
this._panes.clear();
|
|
534
|
+
if (layout.panes !== undefined) {
|
|
535
|
+
this._panes.restore(layout.panes);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Viewport — restore time range so the chart opens at the same position.
|
|
539
|
+
// Price is also restored so custom scales are preserved.
|
|
540
|
+
if (layout.viewport) {
|
|
541
|
+
this._core.restoreViewport(layout.viewport);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
this._bus.emit('layoutLoaded', layout);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Serialises the current chart state to a portable layout object.
|
|
549
|
+
* Safe to `JSON.stringify()` for persistence.
|
|
550
|
+
* Emits `layoutSaved`.
|
|
551
|
+
*/
|
|
552
|
+
saveLayout(): ChartLayout {
|
|
553
|
+
this._assertAlive();
|
|
554
|
+
const panes = this._panes.serialize();
|
|
555
|
+
const layout = LayoutManager.save({
|
|
556
|
+
symbol: this._symbol,
|
|
557
|
+
interval: this._interval,
|
|
558
|
+
theme: this._theme,
|
|
559
|
+
indicators: this._indicators.all(),
|
|
560
|
+
drawings: this._drawings.all(),
|
|
561
|
+
viewport: this._core.getViewport(),
|
|
562
|
+
...(panes.length > 0 ? { panes } : {}),
|
|
563
|
+
});
|
|
564
|
+
this._bus.emit('layoutSaved', layout);
|
|
565
|
+
return layout;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Plugin API ───────────────────────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Attaches a plugin at runtime.
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* ```ts
|
|
575
|
+
* chart.use(new WatermarkPlugin({ text: 'ForgeCharts' }));
|
|
576
|
+
* ```
|
|
577
|
+
*/
|
|
578
|
+
use(plugin: IPlugin): this {
|
|
579
|
+
this._assertAlive();
|
|
580
|
+
this._attachPlugin(plugin);
|
|
581
|
+
return this;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Detaches a plugin by its `id` property.
|
|
586
|
+
* Silently no-ops if the plugin is not attached.
|
|
587
|
+
*/
|
|
588
|
+
eject(pluginId: string): this {
|
|
589
|
+
const idx = this._plugins.findIndex((p) => p.id === pluginId);
|
|
590
|
+
if (idx === -1) return this;
|
|
591
|
+
const [removed] = this._plugins.splice(idx, 1) as [IPlugin];
|
|
592
|
+
removed.onDetach();
|
|
593
|
+
return this;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ─── Event bus ────────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Subscribes to a chart event.
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* chart.on('symbolChanged', ({ symbol }) => {
|
|
604
|
+
* document.title = symbol;
|
|
605
|
+
* });
|
|
606
|
+
* ```
|
|
607
|
+
*/
|
|
608
|
+
on<K extends keyof ChartEventMap>(
|
|
609
|
+
event: K,
|
|
610
|
+
handler: (payload: ChartEventMap[K]) => void,
|
|
611
|
+
): this {
|
|
612
|
+
this._bus.on(event, handler);
|
|
613
|
+
return this;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Unsubscribes a previously registered handler.
|
|
618
|
+
* The handler reference must be the same function passed to `on()`.
|
|
619
|
+
*/
|
|
620
|
+
off<K extends keyof ChartEventMap>(
|
|
621
|
+
event: K,
|
|
622
|
+
handler: (payload: ChartEventMap[K]) => void,
|
|
623
|
+
): this {
|
|
624
|
+
this._bus.off(event, handler);
|
|
625
|
+
return this;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Subscribes to an event for a single invocation.
|
|
630
|
+
* The handler is automatically removed after it fires once.
|
|
631
|
+
*/
|
|
632
|
+
once<K extends keyof ChartEventMap>(
|
|
633
|
+
event: K,
|
|
634
|
+
handler: (payload: ChartEventMap[K]) => void,
|
|
635
|
+
): this {
|
|
636
|
+
this._bus.once(event, handler);
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Tears down the chart instance:
|
|
644
|
+
* 1. Detaches all plugins
|
|
645
|
+
* 2. Destroys the canvas engine (removes DOM elements)
|
|
646
|
+
* 3. Emits `destroyed`
|
|
647
|
+
* 4. Clears all event listeners
|
|
648
|
+
*
|
|
649
|
+
* After `destroy()` every further method call throws `[ForgeCharts] destroyed`.
|
|
650
|
+
*/
|
|
651
|
+
destroy(): void {
|
|
652
|
+
if (this._destroyed) return;
|
|
653
|
+
this._destroyed = true;
|
|
654
|
+
|
|
655
|
+
// Tear down datafeed before destroying the canvas so no tick can arrive
|
|
656
|
+
// after series / canvas are gone
|
|
657
|
+
this._connector?.destroy();
|
|
658
|
+
this._connector = null;
|
|
659
|
+
|
|
660
|
+
for (const plugin of [...this._plugins]) {
|
|
661
|
+
plugin.onDetach();
|
|
662
|
+
}
|
|
663
|
+
this._plugins.length = 0;
|
|
664
|
+
|
|
665
|
+
this._core.destroy();
|
|
666
|
+
this._bus.emit('destroyed', {});
|
|
667
|
+
this._bus.clear();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
// ─── Unmanaged ingestion ───────────────────────────────────────────────────
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Load a full historical bar snapshot in unmanaged mode.
|
|
676
|
+
* Replaces the ingestion engine’s bar array and triggers a chart resync.
|
|
677
|
+
*
|
|
678
|
+
* **Only available in unmanaged mode.** Throws if the active license is managed.
|
|
679
|
+
*
|
|
680
|
+
* Call this once at startup (or after a symbol/interval change) before
|
|
681
|
+
* streaming live candles via `pushCandle()`.
|
|
682
|
+
*
|
|
683
|
+
* @param bars Chronologically ordered (oldest → newest) candle array.
|
|
684
|
+
*/
|
|
685
|
+
setHistory(bars: CandleInput[]): void {
|
|
686
|
+
this._assertAlive();
|
|
687
|
+
this._getIngestion().setHistory(bars);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Push a single live candle in unmanaged mode.
|
|
692
|
+
* Follows standard OHLCV merge rules:
|
|
693
|
+
* - same timestamp → update existing bar
|
|
694
|
+
* - newer timestamp → append new bar
|
|
695
|
+
* - older timestamp → silently ignored
|
|
696
|
+
*
|
|
697
|
+
* **Only available in unmanaged mode.** Throws if the active license is managed.
|
|
698
|
+
*/
|
|
699
|
+
pushCandle(bar: CandleInput): void {
|
|
700
|
+
this._assertAlive();
|
|
701
|
+
this._getIngestion().pushCandle(bar);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ─── Trading overlay ─────────────────────────────────────────────────────
|
|
705
|
+
// Shared between unmanaged and managed modes. In unmanaged mode the client
|
|
706
|
+
// provides state externally; in managed mode the managed trading service
|
|
707
|
+
// will drive these via the same store.
|
|
708
|
+
|
|
709
|
+
/** Returns the shared trading overlay store (read access for renderers). */
|
|
710
|
+
getOverlayStore(): TradingOverlayStore {
|
|
711
|
+
return this._overlayStore;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Alias for {@link getOverlayStore} — canonical name used in documentation
|
|
716
|
+
* and the public SDK contract.
|
|
717
|
+
*/
|
|
718
|
+
getTradingOverlayStore(): TradingOverlayStore {
|
|
719
|
+
return this._overlayStore;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ─ Orders ──
|
|
723
|
+
|
|
724
|
+
/** Replace the entire order set on the chart overlay. */
|
|
725
|
+
setOrders(orders: ChartOrder[]): void {
|
|
726
|
+
this._assertAlive();
|
|
727
|
+
this._overlayStore.setOrders(orders);
|
|
728
|
+
this._core.markDirty();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Insert or update a single order (matched by `order.id`). */
|
|
732
|
+
upsertOrder(order: ChartOrder): void {
|
|
733
|
+
this._assertAlive();
|
|
734
|
+
this._overlayStore.upsertOrder(order);
|
|
735
|
+
this._core.markDirty();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Remove an order line by id. Silent no-op if not found. */
|
|
739
|
+
removeOrder(orderId: string): void {
|
|
740
|
+
this._assertAlive();
|
|
741
|
+
this._overlayStore.removeOrder(orderId);
|
|
742
|
+
this._core.markDirty();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ─ Positions ──
|
|
746
|
+
|
|
747
|
+
/** Replace the entire position set on the chart overlay. */
|
|
748
|
+
setPositions(positions: ChartPosition[]): void {
|
|
749
|
+
this._assertAlive();
|
|
750
|
+
this._overlayStore.setPositions(positions);
|
|
751
|
+
this._core.markDirty();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** Insert or update a single position (matched by `position.id`). */
|
|
755
|
+
upsertPosition(position: ChartPosition): void {
|
|
756
|
+
this._assertAlive();
|
|
757
|
+
this._overlayStore.upsertPosition(position);
|
|
758
|
+
this._core.markDirty();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Remove a position marker by id. Silent no-op if not found. */
|
|
762
|
+
removePosition(positionId: string): void {
|
|
763
|
+
this._assertAlive();
|
|
764
|
+
this._overlayStore.removePosition(positionId);
|
|
765
|
+
this._core.markDirty();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ─ Fills ──
|
|
769
|
+
|
|
770
|
+
/** Append an execution fill marker to the chart. Fills are append-only. */
|
|
771
|
+
pushExecution(fill: ExecutionFill): void {
|
|
772
|
+
this._assertAlive();
|
|
773
|
+
this._overlayStore.pushExecution(fill);
|
|
774
|
+
this._core.markDirty();
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ─── Managed trading ────────────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Returns the managed trading controller for this chart instance.
|
|
781
|
+
*
|
|
782
|
+
* The controller is lazily created the first time this is called.
|
|
783
|
+
* It is bound to this chart’s shared overlay store so all managed trades
|
|
784
|
+
* flow into the same visual layer as unmanaged overlays.
|
|
785
|
+
*
|
|
786
|
+
* Call `chart.getManagedTrading().registerProvider(myProvider)` to wire
|
|
787
|
+
* up an execution backend.
|
|
788
|
+
*
|
|
789
|
+
* **Requires a managed-mode license.** The controller’s methods will throw
|
|
790
|
+
* if called without appropriate capability.
|
|
791
|
+
*
|
|
792
|
+
* @example
|
|
793
|
+
* ```ts
|
|
794
|
+
* const mgd = chart.getManagedTrading();
|
|
795
|
+
* mgd.registerProvider(new RithmicProvider(credentials));
|
|
796
|
+
* await mgd.placeOrder({ symbol: 'ES', side: 'buy', type: 'limit', limitPrice: 5200, qty: 1 });
|
|
797
|
+
* ```
|
|
798
|
+
*/
|
|
799
|
+
getManagedTrading(): ManagedTradingController {
|
|
800
|
+
this._assertAlive();
|
|
801
|
+
if (!this._managedTrading) {
|
|
802
|
+
this._managedTrading = new ManagedTradingController(this._overlayStore);
|
|
803
|
+
}
|
|
804
|
+
return this._managedTrading;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private _attachPlugin(plugin: IPlugin): void {
|
|
808
|
+
this._plugins.push(plugin);
|
|
809
|
+
plugin.onAttach(this);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
private _assertAlive(): void {
|
|
813
|
+
if (this._destroyed) {
|
|
814
|
+
throw new Error('[ForgeCharts] Method called on a destroyed chart instance.');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Returns the UnmanagedIngestion instance, lazily creating it on first call.
|
|
820
|
+
* Wires the ingestion callbacks so live candle updates flow into the chart series.
|
|
821
|
+
*/
|
|
822
|
+
private _getIngestion(): UnmanagedIngestion {
|
|
823
|
+
if (!this._unmanagedIngestion) {
|
|
824
|
+
if (!isUnmanagedMode()) {
|
|
825
|
+
throw new Error(
|
|
826
|
+
'[ForgeCharts] External data ingestion is only available in unmanaged mode.',
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
this._unmanagedIngestion = new UnmanagedIngestion(this._interval, {
|
|
830
|
+
onBarUpdated: (bar) => {
|
|
831
|
+
this._series.update(bar);
|
|
832
|
+
this._bus.emit('barsUpdated', { count: this._series.data().length });
|
|
833
|
+
},
|
|
834
|
+
onResync: (bars) => {
|
|
835
|
+
this._series.setData(bars as unknown as OHLCV[]);
|
|
836
|
+
this._bus.emit('barsUpdated', { count: bars.length });
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
return this._unmanagedIngestion;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ─── Datafeed ─────────────────────────────────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
private _buildConnector(datafeed: IDatafeed): DatafeedConnector {
|
|
846
|
+
return new DatafeedConnector(datafeed, this._series, {
|
|
847
|
+
onLoading: (symbol, interval) => {
|
|
848
|
+
this._bus.emit('dataLoading', { symbol, interval });
|
|
849
|
+
},
|
|
850
|
+
onLoaded: (symbol, interval, count) => {
|
|
851
|
+
this._bus.emit('dataLoaded', { symbol, interval, count });
|
|
852
|
+
this._bus.emit('barsUpdated', { count });
|
|
853
|
+
},
|
|
854
|
+
onError: (symbol, interval, error) => {
|
|
855
|
+
this._bus.emit('dataError', { symbol, interval, error });
|
|
856
|
+
},
|
|
857
|
+
onUpdate: (_bar) => {
|
|
858
|
+
this._bus.emit('barsUpdated', { count: this._series.data().length });
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ─── Module helpers ───────────────────────────────────────────────────────────
|
|
865
|
+
|
|
866
|
+
function _indicatorTypeLabel(type: IndicatorConfig['type']): string {
|
|
867
|
+
switch (type) {
|
|
868
|
+
case 'rsi': return 'RSI';
|
|
869
|
+
case 'macd': return 'MACD';
|
|
870
|
+
case 'volume': return 'Volume';
|
|
871
|
+
case 'sma': return 'SMA';
|
|
872
|
+
case 'ema': return 'EMA';
|
|
873
|
+
case 'wma': return 'WMA';
|
|
874
|
+
case 'bbands': return 'BB';
|
|
875
|
+
case 'stochastic': return 'Stoch';
|
|
876
|
+
case 'atr': return 'ATR';
|
|
877
|
+
case 'obv': return 'OBV';
|
|
878
|
+
case 'script': return 'Script';
|
|
879
|
+
default: return type;
|
|
880
|
+
}
|
|
881
|
+
}
|