@mathieuc/tradingview 3.5.2 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +112 -51
  2. package/dist/chart/chart-session.d.ts +114 -0
  3. package/dist/chart/chart-session.js +339 -0
  4. package/dist/chart/graphics.d.ts +129 -0
  5. package/dist/chart/graphics.js +153 -0
  6. package/dist/chart/index.d.ts +11 -0
  7. package/dist/chart/index.js +6 -0
  8. package/dist/chart/strategy.d.ts +113 -0
  9. package/dist/chart/strategy.js +78 -0
  10. package/dist/chart/study.d.ts +60 -0
  11. package/dist/chart/study.js +179 -0
  12. package/dist/chart/timeframes.d.ts +7 -0
  13. package/dist/chart/timeframes.js +22 -0
  14. package/dist/chart/types.d.ts +105 -0
  15. package/dist/chart/types.js +9 -0
  16. package/dist/client/client.d.ts +89 -0
  17. package/dist/client/client.js +254 -0
  18. package/dist/client/index.d.ts +4 -0
  19. package/dist/client/index.js +2 -0
  20. package/dist/client/transport.d.ts +27 -0
  21. package/dist/client/transport.js +38 -0
  22. package/dist/data/candles.d.ts +40 -0
  23. package/dist/data/candles.js +101 -0
  24. package/dist/data/history.d.ts +61 -0
  25. package/dist/data/history.js +101 -0
  26. package/dist/data/index.d.ts +30 -0
  27. package/dist/data/index.js +16 -0
  28. package/dist/data/indicators.d.ts +54 -0
  29. package/dist/data/indicators.js +139 -0
  30. package/dist/data/operation.d.ts +71 -0
  31. package/dist/data/operation.js +205 -0
  32. package/dist/data/provider.d.ts +23 -0
  33. package/dist/data/provider.js +11 -0
  34. package/dist/data/quotes.d.ts +33 -0
  35. package/dist/data/quotes.js +117 -0
  36. package/dist/data/symbols.d.ts +9 -0
  37. package/dist/data/symbols.js +19 -0
  38. package/dist/errors.d.ts +53 -0
  39. package/dist/errors.js +24 -0
  40. package/dist/events.d.ts +36 -0
  41. package/dist/events.js +70 -0
  42. package/dist/http/account.d.ts +47 -0
  43. package/dist/http/account.js +127 -0
  44. package/dist/http/index.d.ts +11 -0
  45. package/dist/http/index.js +5 -0
  46. package/dist/http/indicators.d.ts +37 -0
  47. package/dist/http/indicators.js +144 -0
  48. package/dist/http/layouts.d.ts +36 -0
  49. package/dist/http/layouts.js +24 -0
  50. package/dist/http/market.d.ts +45 -0
  51. package/dist/http/market.js +59 -0
  52. package/dist/http/pine-permissions.d.ts +29 -0
  53. package/dist/http/pine-permissions.js +56 -0
  54. package/dist/http/request.d.ts +44 -0
  55. package/dist/http/request.js +83 -0
  56. package/dist/index.d.ts +21 -0
  57. package/dist/index.js +19 -0
  58. package/dist/indicators/builtin-indicator.d.ts +17 -0
  59. package/dist/indicators/builtin-indicator.js +75 -0
  60. package/dist/indicators/index.d.ts +8 -0
  61. package/dist/indicators/index.js +2 -0
  62. package/dist/indicators/pine-indicator.d.ts +63 -0
  63. package/dist/indicators/pine-indicator.js +101 -0
  64. package/dist/protocol/compression.d.ts +12 -0
  65. package/dist/protocol/compression.js +83 -0
  66. package/dist/protocol/framing.d.ts +59 -0
  67. package/dist/protocol/framing.js +68 -0
  68. package/dist/protocol/ids.d.ts +2 -0
  69. package/dist/protocol/ids.js +10 -0
  70. package/dist/protocol/index.d.ts +4 -0
  71. package/dist/protocol/index.js +3 -0
  72. package/dist/quote/fields.d.ts +35 -0
  73. package/dist/quote/fields.js +21 -0
  74. package/dist/quote/index.d.ts +4 -0
  75. package/dist/quote/index.js +2 -0
  76. package/dist/quote/quote-session.d.ts +73 -0
  77. package/dist/quote/quote-session.js +205 -0
  78. package/docs/README.es.md +58 -0
  79. package/docs/README.fr.md +58 -0
  80. package/docs/README.pt.md +58 -0
  81. package/docs/data-api.md +285 -0
  82. package/docs/low-level-api.md +213 -0
  83. package/docs/migration-v4.md +128 -0
  84. package/docs/release-4.0.0-beta.1.md +39 -0
  85. package/docs/v4-coverage.md +213 -0
  86. package/package.json +53 -20
  87. package/.env.sample +0 -2
  88. package/.eslintrc.js +0 -28
  89. package/.gitattributes +0 -2
  90. package/docs/DOCS.md +0 -3
  91. package/examples/AllPrivateIndicators.js +0 -44
  92. package/examples/BuiltInIndicator.js +0 -51
  93. package/examples/CustomChartType.js +0 -123
  94. package/examples/CustomTimeframe.js +0 -31
  95. package/examples/Errors.js +0 -155
  96. package/examples/FakeReplayMode.js +0 -40
  97. package/examples/FromToData.js +0 -35
  98. package/examples/GetDrawings.js +0 -27
  99. package/examples/GraphicIndicator.js +0 -42
  100. package/examples/MultipleSyncFetch.js +0 -44
  101. package/examples/PinePermManage.js +0 -67
  102. package/examples/ReplayMode.js +0 -103
  103. package/examples/Search.js +0 -14
  104. package/examples/SimpleChart.js +0 -63
  105. package/examples/UserLogin.js +0 -16
  106. package/main.js +0 -11
  107. package/src/chart/graphicParser.js +0 -308
  108. package/src/chart/session.js +0 -553
  109. package/src/chart/study.js +0 -435
  110. package/src/classes/BuiltInIndicator.js +0 -137
  111. package/src/classes/PineIndicator.js +0 -132
  112. package/src/classes/PinePermManager.js +0 -159
  113. package/src/client.js +0 -298
  114. package/src/miscRequests.js +0 -609
  115. package/src/protocol.js +0 -60
  116. package/src/quote/market.js +0 -132
  117. package/src/quote/session.js +0 -125
  118. package/src/types.js +0 -36
  119. package/src/utils.js +0 -20
  120. package/vite.config.js +0 -7
@@ -0,0 +1,179 @@
1
+ import { TradingViewError } from '../errors.js';
2
+ import { Emitter } from '../events.js';
3
+ import { BuiltInIndicator } from '../indicators/builtin-indicator.js';
4
+ import { PineIndicator } from '../indicators/pine-indicator.js';
5
+ import { decodeCompressed } from '../protocol/compression.js';
6
+ import { createSessionId } from '../protocol/ids.js';
7
+ import { applyGraphicsCommands, parseGraphics, } from './graphics.js';
8
+ import { mergeStrategyReport, } from './strategy.js';
9
+ function assertIndicator(indicator) {
10
+ if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) {
11
+ throw new TradingViewError('INVALID_ARGUMENT', 'The indicator must be a PineIndicator (see getIndicator) or a BuiltInIndicator');
12
+ }
13
+ }
14
+ /** Formats a `study_error` payload into a readable message. */
15
+ function studyErrorMessage(error) {
16
+ if (error && typeof error === 'object' && 'error' in error) {
17
+ const { error: template, ctx } = error;
18
+ if (typeof template === 'string') {
19
+ return template.replace(/\{(\w+)\}/g, (all, key) => (ctx && key in ctx ? String(ctx[key]) : all));
20
+ }
21
+ }
22
+ return typeof error === 'string' ? error : JSON.stringify(error);
23
+ }
24
+ /** An indicator running on a chart. Create it with `chart.createStudy(indicator)`. */
25
+ export class Study extends Emitter {
26
+ id = createSessionId('st');
27
+ #bridge;
28
+ #values = new Map();
29
+ #graphics = {};
30
+ #graphicIndexes = [];
31
+ #strategyReport = { trades: [], history: {}, performance: {} };
32
+ #indicator;
33
+ #removed = false;
34
+ #ready = false;
35
+ /** @internal Use `chart.createStudy()`. */
36
+ constructor(bridge, indicator) {
37
+ super();
38
+ assertIndicator(indicator);
39
+ this.#bridge = bridge;
40
+ this.#indicator = indicator;
41
+ }
42
+ /** @internal */
43
+ create() {
44
+ this.#bridge.send('create_study', [
45
+ this.#bridge.chartId, this.id, 'st1', '$prices', this.#indicator.type, this.#indicator.toStudyInputs(),
46
+ ]);
47
+ }
48
+ /** The indicator definition running in this study. */
49
+ get indicator() {
50
+ return this.#indicator;
51
+ }
52
+ /** True after the first `study_completed`. */
53
+ get isReady() {
54
+ return this.#ready;
55
+ }
56
+ /** True after `remove()` or when the chart is gone. */
57
+ get isRemoved() {
58
+ return this.#removed;
59
+ }
60
+ /** Plot values, oldest first. */
61
+ get values() {
62
+ return [...this.#values.values()].sort((a, b) => a.$time - b.$time);
63
+ }
64
+ /** Drawings (labels, lines, boxes, tables...) produced by the indicator. */
65
+ get graphics() {
66
+ const barsBack = this.#bridge.barsBack();
67
+ return parseGraphics(this.#graphics, this.#graphicIndexes.map((index) => barsBack.get(index)));
68
+ }
69
+ /** Strategy report (Pine strategies only). */
70
+ get strategyReport() {
71
+ return this.#strategyReport;
72
+ }
73
+ /** Replaces the indicator (typically the same script with new inputs). */
74
+ setIndicator(indicator) {
75
+ assertIndicator(indicator);
76
+ if (this.#removed)
77
+ throw new TradingViewError('INVALID_STATE', 'The study is removed');
78
+ this.#indicator = indicator;
79
+ this.#bridge.send('modify_study', [this.#bridge.chartId, this.id, 'st1', indicator.toStudyInputs()]);
80
+ }
81
+ /** Removes the study from the chart. Idempotent. */
82
+ remove() {
83
+ if (this.#removed)
84
+ return;
85
+ this.#bridge.send('remove_study', [this.#bridge.chartId, this.id]);
86
+ this.#removed = true;
87
+ this.#bridge.unregister(this.id);
88
+ }
89
+ /** @internal Called when the chart or connection is gone; `error` is reported if set. */
90
+ dispose(error) {
91
+ if (this.#removed)
92
+ return;
93
+ this.#removed = true;
94
+ if (error)
95
+ this.emit('error', error);
96
+ }
97
+ /** @internal Packets addressed to this study (`study_*`). */
98
+ onPacket(packet) {
99
+ this.#bridge.log('study', this.id, packet);
100
+ switch (packet.m) {
101
+ case 'study_loading':
102
+ this.emit('loading');
103
+ return;
104
+ case 'study_completed':
105
+ this.#ready = true;
106
+ this.emit('ready');
107
+ return;
108
+ case 'study_error':
109
+ this.emit('error', new TradingViewError('STUDY_ERROR', `Study error: ${studyErrorMessage(packet.p[3])}`, {
110
+ details: packet.p.slice(3),
111
+ }));
112
+ return;
113
+ default:
114
+ }
115
+ }
116
+ /** @internal Data from `timescale_update` / `du`. */
117
+ onData(data) {
118
+ if (!data || typeof data !== 'object')
119
+ return;
120
+ const changes = [];
121
+ if (Array.isArray(data.st) && data.st.length > 0) {
122
+ const { plots } = this.#indicator instanceof PineIndicator ? this.#indicator : { plots: undefined };
123
+ for (const row of data.st) {
124
+ const value = {};
125
+ row.v.forEach((plot, i) => {
126
+ if (i === 0) {
127
+ value.$time = plot;
128
+ return;
129
+ }
130
+ const name = plots?.[`plot_${i - 1}`];
131
+ if (name && !(name in value))
132
+ value[name] = plot;
133
+ else
134
+ value[`plot_${i - 1}`] = plot;
135
+ });
136
+ this.#values.set(value.$time, value);
137
+ }
138
+ changes.push('plots');
139
+ }
140
+ const ns = data.ns;
141
+ if (ns?.d) {
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(ns.d);
145
+ }
146
+ catch (error) {
147
+ this.emit('error', new TradingViewError('PARSE_ERROR', 'Unable to parse study data', { cause: error }));
148
+ }
149
+ if (parsed?.graphicsCmds) {
150
+ applyGraphicsCommands(this.#graphics, parsed.graphicsCmds);
151
+ changes.push('graphic');
152
+ }
153
+ if (parsed?.dataCompressed) {
154
+ try {
155
+ const decoded = decodeCompressed(parsed.dataCompressed);
156
+ changes.push(...mergeStrategyReport(this.#strategyReport, decoded?.report));
157
+ }
158
+ catch (error) {
159
+ this.emit('error', new TradingViewError('PARSE_ERROR', 'Unable to parse compressed strategy report', {
160
+ cause: error,
161
+ }));
162
+ }
163
+ }
164
+ if (parsed?.data?.report) {
165
+ try {
166
+ changes.push(...mergeStrategyReport(this.#strategyReport, parsed.data.report));
167
+ }
168
+ catch (error) {
169
+ this.emit('error', new TradingViewError('PARSE_ERROR', 'Unable to parse strategy report', {
170
+ cause: error,
171
+ }));
172
+ }
173
+ }
174
+ }
175
+ if (Array.isArray(ns?.indexes))
176
+ this.#graphicIndexes = ns.indexes;
177
+ this.emit('update', changes);
178
+ }
179
+ }
@@ -0,0 +1,7 @@
1
+ import type { Timeframe } from './types.js';
2
+ /**
3
+ * Approximate duration of one bar in seconds, or `undefined` for
4
+ * resolutions that are not time-based (ticks, ranges...).
5
+ * Months count as 30 days.
6
+ */
7
+ export declare function timeframeSeconds(timeframe: Timeframe): number | undefined;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Approximate duration of one bar in seconds, or `undefined` for
3
+ * resolutions that are not time-based (ticks, ranges...).
4
+ * Months count as 30 days.
5
+ */
6
+ export function timeframeSeconds(timeframe) {
7
+ const match = /^(\d*)([SDWM]?)$/i.exec(String(timeframe).trim());
8
+ if (!match)
9
+ return undefined;
10
+ const [, amountText, unitText] = match;
11
+ const amount = amountText ? Number(amountText) : 1;
12
+ if (!Number.isFinite(amount) || amount <= 0)
13
+ return undefined;
14
+ switch (unitText.toUpperCase()) {
15
+ case '': return amountText ? amount * 60 : undefined;
16
+ case 'S': return amount;
17
+ case 'D': return amount * 86_400;
18
+ case 'W': return amount * 604_800;
19
+ case 'M': return amount * 2_592_000;
20
+ default: return undefined;
21
+ }
22
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Exchange-qualified symbol such as `BINANCE:BTCEUR` or `NASDAQ:AAPL`.
3
+ * A bare ticker (`BTCEUR`) lets TradingView pick an exchange.
4
+ */
5
+ export type MarketSymbol = string;
6
+ /**
7
+ * Bar resolution: minutes (`1`, `5`, `60`, `240`), seconds (`1S`), or
8
+ * days/weeks/months (`D`, `1D`, `W`, `M`...). Custom values such as `20`
9
+ * or `1S` need an account with access to them.
10
+ */
11
+ export type Timeframe = '1S' | '5S' | '10S' | '15S' | '30S' | '1' | '3' | '5' | '15' | '30' | '45' | '60' | '120' | '180' | '240' | '1D' | '1W' | '1M' | 'D' | 'W' | 'M' | '3M' | '6M' | '12M' | (string & {});
12
+ /** Chart timezone. `exchange` uses the exchange timezone. Any IANA name TradingView supports is accepted. */
13
+ export type Timezone = 'Etc/UTC' | 'exchange' | 'Pacific/Honolulu' | 'America/Juneau' | 'America/Los_Angeles' | 'America/Phoenix' | 'America/Vancouver' | 'US/Mountain' | 'America/El_Salvador' | 'America/Bogota' | 'America/Chicago' | 'America/Lima' | 'America/Mexico_City' | 'America/Caracas' | 'America/New_York' | 'America/Toronto' | 'America/Argentina/Buenos_Aires' | 'America/Santiago' | 'America/Sao_Paulo' | 'Atlantic/Reykjavik' | 'Europe/Dublin' | 'Africa/Lagos' | 'Europe/Lisbon' | 'Europe/London' | 'Europe/Amsterdam' | 'Europe/Belgrade' | 'Europe/Berlin' | 'Europe/Brussels' | 'Europe/Copenhagen' | 'Africa/Johannesburg' | 'Africa/Cairo' | 'Europe/Luxembourg' | 'Europe/Madrid' | 'Europe/Malta' | 'Europe/Oslo' | 'Europe/Paris' | 'Europe/Rome' | 'Europe/Stockholm' | 'Europe/Warsaw' | 'Europe/Zurich' | 'Europe/Athens' | 'Asia/Bahrain' | 'Europe/Helsinki' | 'Europe/Istanbul' | 'Asia/Jerusalem' | 'Asia/Kuwait' | 'Europe/Moscow' | 'Asia/Qatar' | 'Europe/Riga' | 'Asia/Riyadh' | 'Europe/Tallinn' | 'Europe/Vilnius' | 'Asia/Tehran' | 'Asia/Dubai' | 'Asia/Muscat' | 'Asia/Ashkhabad' | 'Asia/Kolkata' | 'Asia/Almaty' | 'Asia/Bangkok' | 'Asia/Jakarta' | 'Asia/Ho_Chi_Minh' | 'Asia/Chongqing' | 'Asia/Hong_Kong' | 'Australia/Perth' | 'Asia/Shanghai' | 'Asia/Singapore' | 'Asia/Taipei' | 'Asia/Seoul' | 'Asia/Tokyo' | 'Australia/Brisbane' | 'Australia/Adelaide' | 'Australia/Sydney' | 'Pacific/Norfolk' | 'Pacific/Auckland' | 'Pacific/Fakaofo' | 'Pacific/Chatham' | (string & {});
14
+ /** Custom bar types computed by TradingView. */
15
+ export type ChartType = 'HeikinAshi' | 'Renko' | 'LineBreak' | 'Kagi' | 'PointAndFigure' | 'Range';
16
+ /** Server study used for each chart type. */
17
+ export declare const CHART_TYPE_STUDIES: Record<ChartType, string>;
18
+ /** Inputs for custom chart types. */
19
+ export interface ChartTypeInputs {
20
+ /** Renko/Kagi/PointAndFigure ATR length. */
21
+ atrLength?: number;
22
+ /** Renko/LineBreak/Kagi source. */
23
+ source?: 'open' | 'high' | 'low' | 'close' | 'hl2' | 'hlc3' | 'ohlc4';
24
+ /** Renko/Kagi/PointAndFigure style. */
25
+ style?: 'ATR' | (string & {});
26
+ /** Renko/PointAndFigure box size. */
27
+ boxSize?: number;
28
+ /** Kagi/PointAndFigure reversal amount. */
29
+ reversalAmount?: number;
30
+ /** Renko/PointAndFigure sources. */
31
+ sources?: 'Close' | (string & {});
32
+ /** Renko wicks. */
33
+ wicks?: boolean;
34
+ /** LineBreak number of lines. */
35
+ lb?: number;
36
+ /** PointAndFigure one-step-back building. */
37
+ oneStepBackBuilding?: boolean;
38
+ /** Range phantom bars. */
39
+ phantomBars?: boolean;
40
+ /** Range size. */
41
+ range?: number;
42
+ [input: string]: unknown;
43
+ }
44
+ /** Price adjustment. */
45
+ export type Adjustment = 'splits' | 'dividends' | 'none' | (string & {});
46
+ /** Trading session. */
47
+ export type TradingSession = 'regular' | 'extended' | (string & {});
48
+ /** One bar. `time` is the bar open time as a Unix timestamp in seconds. */
49
+ export interface Candle {
50
+ time: number;
51
+ open: number;
52
+ high: number;
53
+ low: number;
54
+ close: number;
55
+ volume: number;
56
+ }
57
+ export interface Subsession {
58
+ id: string;
59
+ description: string;
60
+ private: boolean;
61
+ session: string;
62
+ 'session-correction': string;
63
+ 'session-display': string;
64
+ }
65
+ /** Symbol metadata from `symbol_resolved`. Fields vary by market; unknown ones are kept. */
66
+ export interface SymbolInfo {
67
+ /** Series ID used by this chart (`ser_1`). */
68
+ series_id: string;
69
+ name: string;
70
+ full_name: string;
71
+ pro_name: string;
72
+ description: string;
73
+ short_description: string;
74
+ exchange: string;
75
+ listed_exchange: string;
76
+ provider_id: string;
77
+ base_currency?: string;
78
+ base_currency_id?: string;
79
+ currency_id: string;
80
+ currency_code: string;
81
+ type: string;
82
+ timezone: string;
83
+ session: string;
84
+ session_display?: string;
85
+ subsession_id?: string;
86
+ subsessions?: Subsession[];
87
+ pricescale: number;
88
+ pointvalue?: number;
89
+ minmov: number;
90
+ minmove2?: number;
91
+ fractional: boolean;
92
+ has_intraday: boolean;
93
+ has_adjustment?: boolean;
94
+ has_extended_hours?: boolean;
95
+ is_tradable: boolean;
96
+ is_replayable?: boolean;
97
+ allowed_adjustment?: string;
98
+ variable_tick_size?: string;
99
+ pro_perm?: string;
100
+ typespecs?: string[];
101
+ resolutions?: string[];
102
+ aliases?: string[];
103
+ alternatives?: string[];
104
+ [key: string]: unknown;
105
+ }
@@ -0,0 +1,9 @@
1
+ /** Server study used for each chart type. */
2
+ export const CHART_TYPE_STUDIES = {
3
+ HeikinAshi: 'BarSetHeikenAshi@tv-basicstudies-60!',
4
+ Renko: 'BarSetRenko@tv-prostudies-40!',
5
+ LineBreak: 'BarSetPriceBreak@tv-prostudies-34!',
6
+ Kagi: 'BarSetKagi@tv-prostudies-34!',
7
+ PointAndFigure: 'BarSetPnF@tv-prostudies-34!',
8
+ Range: 'BarSetRange@tv-basicstudies-72!',
9
+ };
@@ -0,0 +1,89 @@
1
+ import { TradingViewError } from '../errors.js';
2
+ import { Emitter } from '../events.js';
3
+ import { type Credentials } from '../http/request.js';
4
+ import { type ServerHello, type ServerPacket } from '../protocol/framing.js';
5
+ import { type TransportFactory } from './transport.js';
6
+ import { ChartSession } from '../chart/chart-session.js';
7
+ import { QuoteSession, type QuoteSessionOptions } from '../quote/quote-session.js';
8
+ /** Websocket server. `prodata` is used by paid accounts, `widgetdata` by widgets. */
9
+ export type ServerName = 'data' | 'prodata' | 'widgetdata' | (string & {});
10
+ export type DebugOption = boolean | ((...args: unknown[]) => void);
11
+ export interface ClientOptions {
12
+ /** Account cookies. Without them, the client uses anonymous (delayed/limited) access. */
13
+ credentials?: Credentials;
14
+ /** Websocket auth token (`User.authToken`). Skips the account lookup done with `credentials`. */
15
+ authToken?: string;
16
+ /** Websocket server. Default: `data`. */
17
+ server?: ServerName;
18
+ /** Page used to load the account from `credentials`, e.g. `https://fr.tradingview.com/`. */
19
+ location?: string;
20
+ /** Extra websocket headers. */
21
+ headers?: Record<string, string>;
22
+ /** Logs packets: `true` uses `console.log`, a function receives the log arguments. */
23
+ debug?: DebugOption;
24
+ /** Custom websocket transport (proxies, tests). */
25
+ transport?: TransportFactory;
26
+ /** Custom `fetch` for the account lookup. */
27
+ fetch?: typeof fetch;
28
+ /** Time allowed to open and authenticate the connection. Default: 20 000 ms. */
29
+ connectTimeoutMs?: number;
30
+ }
31
+ export interface ClientEvents {
32
+ [event: string]: unknown[];
33
+ /** The websocket is open. */
34
+ open: [];
35
+ /** The server greeting was received. */
36
+ hello: [hello: ServerHello];
37
+ /** The auth token was sent; queued packets are now flowing. */
38
+ ready: [];
39
+ /** A server heartbeat was received (the client answers automatically). */
40
+ heartbeat: [id: number];
41
+ /** A packet not addressed to any session of this client. */
42
+ packet: [packet: ServerPacket];
43
+ /** The websocket closed. */
44
+ close: [code?: number, reason?: string];
45
+ error: [error: TradingViewError];
46
+ }
47
+ /** @internal Implemented by chart, replay and quote sessions. */
48
+ export interface SessionHandler {
49
+ onPacket(packet: ServerPacket): void;
50
+ /** `expected` is true when the connection was closed with `client.close()`. */
51
+ onClose?(error: TradingViewError, expected: boolean): void;
52
+ }
53
+ /**
54
+ * One websocket connection to TradingView. Chart and quote sessions are
55
+ * multiplexed over it.
56
+ *
57
+ * The connection opens immediately; packets are queued until it is open and
58
+ * authenticated. Always call `close()` when you are done.
59
+ */
60
+ export declare class TradingViewClient extends Emitter<ClientEvents> {
61
+ #private;
62
+ readonly ready: Promise<void>;
63
+ constructor(options?: ClientOptions);
64
+ /** True while the websocket is open. */
65
+ get isOpen(): boolean;
66
+ /** True once the auth token has been sent. */
67
+ get isAuthenticated(): boolean;
68
+ /** True after `close()` or a disconnection. */
69
+ get isClosed(): boolean;
70
+ /** Server greeting, once received. */
71
+ get serverInfo(): ServerHello | undefined;
72
+ /** Creates a chart session on this connection. */
73
+ createChart(): ChartSession;
74
+ /** Creates a quote session on this connection. */
75
+ createQuoteSession(options?: QuoteSessionOptions): QuoteSession;
76
+ /**
77
+ * Sends a raw packet. Packets are queued until the connection is ready.
78
+ * Prefer session methods; this is an escape hatch for unsupported commands.
79
+ */
80
+ send(method: string, params?: readonly unknown[]): void;
81
+ /** @internal Registers a session to receive its packets. */
82
+ registerSession(id: string, handler: SessionHandler): void;
83
+ /** @internal */
84
+ unregisterSession(id: string): void;
85
+ /** @internal Debug logger shared with sessions. */
86
+ log(...args: unknown[]): void;
87
+ /** Closes the connection. Resolves once the websocket is closed. Idempotent. */
88
+ close(): Promise<void>;
89
+ }
@@ -0,0 +1,254 @@
1
+ import { TradingViewError, toTradingViewError } from '../errors.js';
2
+ import { Emitter } from '../events.js';
3
+ import { getUser } from '../http/account.js';
4
+ import { DEFAULT_USER_AGENT } from '../http/request.js';
5
+ import { decodeFrames, encodeHeartbeat, encodePacket, } from '../protocol/framing.js';
6
+ import { wsTransport } from './transport.js';
7
+ import { ChartSession } from '../chart/chart-session.js';
8
+ import { QuoteSession } from '../quote/quote-session.js';
9
+ /**
10
+ * One websocket connection to TradingView. Chart and quote sessions are
11
+ * multiplexed over it.
12
+ *
13
+ * The connection opens immediately; packets are queued until it is open and
14
+ * authenticated. Always call `close()` when you are done.
15
+ */
16
+ export class TradingViewClient extends Emitter {
17
+ #transport;
18
+ #sessions = new Map();
19
+ #queue = [];
20
+ #log;
21
+ ready;
22
+ #authenticated = false;
23
+ #authToken;
24
+ #closed = false;
25
+ #closing = false;
26
+ #closeRequested = false;
27
+ #failure;
28
+ #hello;
29
+ #resolveReady;
30
+ #rejectReady;
31
+ #readyTimer;
32
+ #closeWaiters = [];
33
+ constructor(options = {}) {
34
+ super();
35
+ if (options.debug === true)
36
+ this.#log = (...args) => console.log('[tradingview]', ...args);
37
+ else if (typeof options.debug === 'function')
38
+ this.#log = options.debug;
39
+ this.ready = new Promise((resolve, reject) => {
40
+ this.#resolveReady = resolve;
41
+ this.#rejectReady = reject;
42
+ });
43
+ this.ready.catch(() => { });
44
+ const timeout = options.connectTimeoutMs ?? 20_000;
45
+ this.#readyTimer = setTimeout(() => {
46
+ this.#fail(new TradingViewError('TIMEOUT', `Connection not ready after ${timeout} ms`));
47
+ }, timeout);
48
+ const server = options.server ?? 'data';
49
+ const factory = options.transport ?? wsTransport;
50
+ this.#transport = factory({
51
+ url: `wss://${server}.tradingview.com/socket.io/websocket?from=chart&type=chart`,
52
+ origin: 'https://www.tradingview.com',
53
+ headers: {
54
+ 'User-Agent': DEFAULT_USER_AGENT,
55
+ 'Accept-Language': 'en-US,en;q=0.9',
56
+ 'Cache-Control': 'no-cache',
57
+ Pragma: 'no-cache',
58
+ ...options.headers,
59
+ },
60
+ }, {
61
+ onOpen: () => this.#onOpen(),
62
+ onMessage: (data) => this.#onMessage(data),
63
+ onClose: (code, reason) => this.#onClose(code, reason),
64
+ onError: (error) => {
65
+ if (this.#closing)
66
+ return;
67
+ this.#fail(new TradingViewError('CONNECTION_ERROR', `WebSocket error: ${error.message}`, { cause: error }));
68
+ },
69
+ });
70
+ if (options.authToken) {
71
+ this.#authToken = options.authToken;
72
+ }
73
+ else if (options.credentials?.session) {
74
+ getUser(options.credentials, { location: options.location, fetch: options.fetch })
75
+ .then((user) => {
76
+ if (this.#closed)
77
+ return;
78
+ this.#authToken = user.authToken;
79
+ this.#authenticate();
80
+ })
81
+ .catch((error) => {
82
+ this.#fail(new TradingViewError('AUTH_ERROR', `Credentials error: ${toTradingViewError(error).message}`, {
83
+ cause: error,
84
+ }));
85
+ });
86
+ }
87
+ else {
88
+ this.#authToken = 'unauthorized_user_token';
89
+ }
90
+ }
91
+ /** True while the websocket is open. */
92
+ get isOpen() {
93
+ return !this.#closed && this.#transport.isOpen;
94
+ }
95
+ /** True once the auth token has been sent. */
96
+ get isAuthenticated() {
97
+ return this.#authenticated;
98
+ }
99
+ /** True after `close()` or a disconnection. */
100
+ get isClosed() {
101
+ return this.#closed;
102
+ }
103
+ /** Server greeting, once received. */
104
+ get serverInfo() {
105
+ return this.#hello;
106
+ }
107
+ /** Creates a chart session on this connection. */
108
+ createChart() {
109
+ return new ChartSession(this);
110
+ }
111
+ /** Creates a quote session on this connection. */
112
+ createQuoteSession(options = {}) {
113
+ return new QuoteSession(this, options);
114
+ }
115
+ /**
116
+ * Sends a raw packet. Packets are queued until the connection is ready.
117
+ * Prefer session methods; this is an escape hatch for unsupported commands.
118
+ */
119
+ send(method, params = []) {
120
+ if (this.#closed) {
121
+ throw new TradingViewError('INVALID_STATE', `Cannot send '${method}': the client is closed`);
122
+ }
123
+ this.#queue.push(encodePacket(method, params));
124
+ this.#flush();
125
+ }
126
+ /** @internal Registers a session to receive its packets. */
127
+ registerSession(id, handler) {
128
+ this.#sessions.set(id, handler);
129
+ }
130
+ /** @internal */
131
+ unregisterSession(id) {
132
+ this.#sessions.delete(id);
133
+ }
134
+ /** @internal Debug logger shared with sessions. */
135
+ log(...args) {
136
+ this.#log?.(...args);
137
+ }
138
+ /** Closes the connection. Resolves once the websocket is closed. Idempotent. */
139
+ close() {
140
+ if (this.#closed)
141
+ return Promise.resolve();
142
+ return new Promise((resolve) => {
143
+ this.#closeWaiters.push(resolve);
144
+ this.#closeRequested = true;
145
+ if (this.#closing)
146
+ return;
147
+ this.#closing = true;
148
+ // Do not hang if the server never acknowledges the close.
149
+ setTimeout(() => this.#onClose(1000, 'Close timeout'), 3_000).unref?.();
150
+ this.#transport.close();
151
+ });
152
+ }
153
+ #onOpen() {
154
+ this.#log?.('open');
155
+ this.emit('open');
156
+ this.#authenticate();
157
+ }
158
+ #authenticate() {
159
+ if (this.#authenticated || !this.#authToken || !this.#transport.isOpen || this.#closing)
160
+ return;
161
+ this.#transport.send(encodePacket('set_auth_token', [this.#authToken]));
162
+ this.#authenticated = true;
163
+ clearTimeout(this.#readyTimer);
164
+ this.#resolveReady();
165
+ this.emit('ready');
166
+ this.#flush();
167
+ }
168
+ #flush() {
169
+ while (this.#authenticated && this.#transport.isOpen && this.#queue.length > 0) {
170
+ const packet = this.#queue.shift();
171
+ this.#log?.('send', packet);
172
+ this.#transport.send(packet);
173
+ }
174
+ }
175
+ #onMessage(data) {
176
+ if (this.#closed)
177
+ return;
178
+ for (const frame of decodeFrames(data)) {
179
+ if (frame.type === 'heartbeat') {
180
+ if (this.#transport.isOpen)
181
+ this.#transport.send(encodeHeartbeat(frame.id));
182
+ this.emit('heartbeat', frame.id);
183
+ continue;
184
+ }
185
+ if (frame.type === 'invalid') {
186
+ this.#log?.('invalid frame', frame.raw);
187
+ continue;
188
+ }
189
+ if (frame.type === 'data') {
190
+ this.#log?.('data', frame.data);
191
+ const hello = frame.data;
192
+ if (hello && typeof hello === 'object' && 'session_id' in hello) {
193
+ this.#hello = hello;
194
+ this.emit('hello', hello);
195
+ }
196
+ continue;
197
+ }
198
+ const { packet } = frame;
199
+ this.#log?.('packet', packet);
200
+ if (packet.m === 'protocol_error') {
201
+ this.emit('error', new TradingViewError('PROTOCOL_ERROR', `Protocol error: ${String(packet.p[0] ?? '')}`, {
202
+ details: packet.p,
203
+ }));
204
+ this.#transport.close();
205
+ continue;
206
+ }
207
+ const sessionId = packet.p[0];
208
+ const session = typeof sessionId === 'string' ? this.#sessions.get(sessionId) : undefined;
209
+ if (session)
210
+ session.onPacket(packet);
211
+ else
212
+ this.emit('packet', packet);
213
+ }
214
+ }
215
+ #fail(error) {
216
+ if (this.#closed)
217
+ return;
218
+ clearTimeout(this.#readyTimer);
219
+ const wasReady = this.#authenticated;
220
+ this.#failure ??= error;
221
+ this.#rejectReady(error);
222
+ // Before readiness, `ready` rejects with this error: only notify explicit listeners.
223
+ if (wasReady || this.hasListeners('error'))
224
+ this.emit('error', error);
225
+ if (!wasReady || error.code === 'AUTH_ERROR' || error.code === 'CONNECTION_ERROR') {
226
+ this.#closing = true;
227
+ this.#transport.close();
228
+ // Some transports never emit close after a failed handshake.
229
+ setTimeout(() => this.#onClose(undefined, error.message), 3_000).unref?.();
230
+ }
231
+ }
232
+ #onClose(code, reason) {
233
+ if (this.#closed)
234
+ return;
235
+ this.#closed = true;
236
+ this.#authenticated = false;
237
+ clearTimeout(this.#readyTimer);
238
+ this.#log?.('close', code, reason);
239
+ const cause = this.#failure;
240
+ const why = cause?.message ?? reason;
241
+ const error = new TradingViewError('DISCONNECTED', `Connection closed${why ? `: ${why}` : ''}`, {
242
+ details: { code, reason },
243
+ cause,
244
+ });
245
+ this.#rejectReady(error);
246
+ for (const session of [...this.#sessions.values()])
247
+ session.onClose?.(error, this.#closeRequested);
248
+ this.#sessions.clear();
249
+ this.#queue.length = 0;
250
+ this.emit('close', code, reason);
251
+ for (const resolve of this.#closeWaiters.splice(0))
252
+ resolve();
253
+ }
254
+ }
@@ -0,0 +1,4 @@
1
+ export { TradingViewClient } from './client.js';
2
+ export type { ClientEvents, ClientOptions, DebugOption, ServerName, SessionHandler, } from './client.js';
3
+ export { wsTransport } from './transport.js';
4
+ export type { Transport, TransportFactory, TransportHandlers, TransportRequest, } from './transport.js';
@@ -0,0 +1,2 @@
1
+ export { TradingViewClient } from './client.js';
2
+ export { wsTransport } from './transport.js';