@mathieuc/tradingview 3.5.2 → 4.0.0-beta.0
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/README.md +112 -51
- package/dist/chart/chart-session.d.ts +114 -0
- package/dist/chart/chart-session.js +339 -0
- package/dist/chart/graphics.d.ts +129 -0
- package/dist/chart/graphics.js +153 -0
- package/dist/chart/index.d.ts +11 -0
- package/dist/chart/index.js +6 -0
- package/dist/chart/strategy.d.ts +100 -0
- package/dist/chart/strategy.js +51 -0
- package/dist/chart/study.d.ts +60 -0
- package/dist/chart/study.js +171 -0
- package/dist/chart/timeframes.d.ts +7 -0
- package/dist/chart/timeframes.js +22 -0
- package/dist/chart/types.d.ts +105 -0
- package/dist/chart/types.js +9 -0
- package/dist/client/client.d.ts +89 -0
- package/dist/client/client.js +254 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +2 -0
- package/dist/client/transport.d.ts +27 -0
- package/dist/client/transport.js +38 -0
- package/dist/data/candles.d.ts +40 -0
- package/dist/data/candles.js +101 -0
- package/dist/data/history.d.ts +61 -0
- package/dist/data/history.js +101 -0
- package/dist/data/index.d.ts +28 -0
- package/dist/data/index.js +15 -0
- package/dist/data/indicators.d.ts +54 -0
- package/dist/data/indicators.js +139 -0
- package/dist/data/operation.d.ts +71 -0
- package/dist/data/operation.js +205 -0
- package/dist/data/provider.d.ts +23 -0
- package/dist/data/provider.js +11 -0
- package/dist/data/quotes.d.ts +33 -0
- package/dist/data/quotes.js +117 -0
- package/dist/data/symbols.d.ts +9 -0
- package/dist/data/symbols.js +19 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.js +24 -0
- package/dist/events.d.ts +36 -0
- package/dist/events.js +70 -0
- package/dist/http/account.d.ts +47 -0
- package/dist/http/account.js +127 -0
- package/dist/http/index.d.ts +11 -0
- package/dist/http/index.js +5 -0
- package/dist/http/indicators.d.ts +37 -0
- package/dist/http/indicators.js +144 -0
- package/dist/http/layouts.d.ts +36 -0
- package/dist/http/layouts.js +24 -0
- package/dist/http/market.d.ts +45 -0
- package/dist/http/market.js +59 -0
- package/dist/http/pine-permissions.d.ts +29 -0
- package/dist/http/pine-permissions.js +56 -0
- package/dist/http/request.d.ts +44 -0
- package/dist/http/request.js +83 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +19 -0
- package/dist/indicators/builtin-indicator.d.ts +17 -0
- package/dist/indicators/builtin-indicator.js +75 -0
- package/dist/indicators/index.d.ts +8 -0
- package/dist/indicators/index.js +2 -0
- package/dist/indicators/pine-indicator.d.ts +63 -0
- package/dist/indicators/pine-indicator.js +101 -0
- package/dist/protocol/compression.d.ts +12 -0
- package/dist/protocol/compression.js +83 -0
- package/dist/protocol/framing.d.ts +59 -0
- package/dist/protocol/framing.js +68 -0
- package/dist/protocol/ids.d.ts +2 -0
- package/dist/protocol/ids.js +10 -0
- package/dist/protocol/index.d.ts +4 -0
- package/dist/protocol/index.js +3 -0
- package/dist/quote/fields.d.ts +35 -0
- package/dist/quote/fields.js +21 -0
- package/dist/quote/index.d.ts +4 -0
- package/dist/quote/index.js +2 -0
- package/dist/quote/quote-session.d.ts +73 -0
- package/dist/quote/quote-session.js +205 -0
- package/docs/README.es.md +58 -0
- package/docs/README.fr.md +58 -0
- package/docs/README.pt.md +58 -0
- package/docs/data-api.md +229 -0
- package/docs/low-level-api.md +213 -0
- package/docs/migration-v4.md +128 -0
- package/docs/v4-coverage.md +192 -0
- package/docs/v4-reliability.md +33 -0
- package/package.json +55 -20
- package/.env.sample +0 -2
- package/.eslintrc.js +0 -28
- package/.gitattributes +0 -2
- package/docs/DOCS.md +0 -3
- package/examples/AllPrivateIndicators.js +0 -44
- package/examples/BuiltInIndicator.js +0 -51
- package/examples/CustomChartType.js +0 -123
- package/examples/CustomTimeframe.js +0 -31
- package/examples/Errors.js +0 -155
- package/examples/FakeReplayMode.js +0 -40
- package/examples/FromToData.js +0 -35
- package/examples/GetDrawings.js +0 -27
- package/examples/GraphicIndicator.js +0 -42
- package/examples/MultipleSyncFetch.js +0 -44
- package/examples/PinePermManage.js +0 -67
- package/examples/ReplayMode.js +0 -103
- package/examples/Search.js +0 -14
- package/examples/SimpleChart.js +0 -63
- package/examples/UserLogin.js +0 -16
- package/main.js +0 -11
- package/src/chart/graphicParser.js +0 -308
- package/src/chart/session.js +0 -553
- package/src/chart/study.js +0 -435
- package/src/classes/BuiltInIndicator.js +0 -137
- package/src/classes/PineIndicator.js +0 -132
- package/src/classes/PinePermManager.js +0 -159
- package/src/client.js +0 -298
- package/src/miscRequests.js +0 -609
- package/src/protocol.js +0 -60
- package/src/quote/market.js +0 -132
- package/src/quote/session.js +0 -125
- package/src/types.js +0 -36
- package/src/utils.js +0 -20
- package/vite.config.js +0 -7
|
@@ -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,27 @@
|
|
|
1
|
+
/** Callbacks a transport must call. */
|
|
2
|
+
export interface TransportHandlers {
|
|
3
|
+
onOpen(): void;
|
|
4
|
+
onMessage(data: string): void;
|
|
5
|
+
onClose(code?: number, reason?: string): void;
|
|
6
|
+
onError(error: Error): void;
|
|
7
|
+
}
|
|
8
|
+
/** Minimal websocket abstraction used by `TradingViewClient`. */
|
|
9
|
+
export interface Transport {
|
|
10
|
+
/** True when the connection is open and can send. */
|
|
11
|
+
readonly isOpen: boolean;
|
|
12
|
+
send(data: string): void;
|
|
13
|
+
/** Starts a graceful close. `onClose` must be called once closed. */
|
|
14
|
+
close(): void;
|
|
15
|
+
}
|
|
16
|
+
export interface TransportRequest {
|
|
17
|
+
url: string;
|
|
18
|
+
origin: string;
|
|
19
|
+
headers: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Creates a transport. The default uses the `ws` package (Node and Bun).
|
|
23
|
+
* Provide your own to use a proxy, another websocket library or a test double.
|
|
24
|
+
*/
|
|
25
|
+
export type TransportFactory = (request: TransportRequest, handlers: TransportHandlers) => Transport;
|
|
26
|
+
/** Default transport based on the `ws` package. */
|
|
27
|
+
export declare const wsTransport: TransportFactory;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import WebSocket from 'ws';
|
|
2
|
+
function toText(data) {
|
|
3
|
+
if (typeof data === 'string')
|
|
4
|
+
return data;
|
|
5
|
+
if (Array.isArray(data))
|
|
6
|
+
return Buffer.concat(data).toString('utf8');
|
|
7
|
+
if (data instanceof ArrayBuffer)
|
|
8
|
+
return Buffer.from(data).toString('utf8');
|
|
9
|
+
return data.toString('utf8');
|
|
10
|
+
}
|
|
11
|
+
/** Default transport based on the `ws` package. */
|
|
12
|
+
export const wsTransport = (request, handlers) => {
|
|
13
|
+
// Bun's `ws` implementation ignores the `origin` option: also send it as a header.
|
|
14
|
+
const socket = new WebSocket(request.url, {
|
|
15
|
+
origin: request.origin,
|
|
16
|
+
headers: { Origin: request.origin, ...request.headers },
|
|
17
|
+
});
|
|
18
|
+
socket.on('open', () => handlers.onOpen());
|
|
19
|
+
socket.on('message', (data) => handlers.onMessage(toText(data)));
|
|
20
|
+
socket.on('close', (code, reason) => handlers.onClose(code, reason.toString()));
|
|
21
|
+
socket.on('error', (error) => handlers.onError(error));
|
|
22
|
+
return {
|
|
23
|
+
get isOpen() {
|
|
24
|
+
return socket.readyState === WebSocket.OPEN;
|
|
25
|
+
},
|
|
26
|
+
send(data) {
|
|
27
|
+
socket.send(data);
|
|
28
|
+
},
|
|
29
|
+
close() {
|
|
30
|
+
if (socket.readyState === WebSocket.CLOSED)
|
|
31
|
+
return;
|
|
32
|
+
if (socket.readyState === WebSocket.CONNECTING)
|
|
33
|
+
socket.terminate();
|
|
34
|
+
else
|
|
35
|
+
socket.close();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Candle, SymbolInfo } from '../chart/types.js';
|
|
2
|
+
import type { CandleSourceWorker, MarketDataProvider } from './provider.js';
|
|
3
|
+
import { type ChartQuery, type RangeQuery } from './history.js';
|
|
4
|
+
import { type OperationOptions, type WatchHandlers, type Watcher } from './operation.js';
|
|
5
|
+
export interface CandleQuery extends ChartQuery, RangeQuery, OperationOptions {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Fetches candles once, oldest first, then releases every resource.
|
|
9
|
+
*
|
|
10
|
+
* - `{ symbol, timeframe, count }`: the `count` most recent bars (deep history
|
|
11
|
+
* is loaded automatically when `count` exceeds one server batch).
|
|
12
|
+
* - `{ symbol, timeframe, from, to }`: every bar in the range.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const candles = await getCandles({ symbol: 'BINANCE:BTCUSDT', timeframe: '60', count: 500 });
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCandles(query: CandleQuery | string, provider?: MarketDataProvider): Promise<Candle[]>;
|
|
18
|
+
export type WatchCandlesQuery = ChartQuery & OperationOptions;
|
|
19
|
+
export interface CandleHandlers extends WatchHandlers {
|
|
20
|
+
/** Receives a fresh immutable snapshot (oldest first) on every update. */
|
|
21
|
+
onData(candles: readonly Candle[]): void;
|
|
22
|
+
}
|
|
23
|
+
export interface CandleWatcher extends Watcher {
|
|
24
|
+
/** Latest snapshot, oldest first (at most `count` bars). */
|
|
25
|
+
readonly latest: readonly Candle[];
|
|
26
|
+
/** Symbol metadata. */
|
|
27
|
+
readonly symbolInfo: SymbolInfo | undefined;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Streams candles in real time. Resolves once the initial history is loaded
|
|
31
|
+
* (and already passed to `onData`); call `stop()` when finished.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const watcher = await watchCandles({ symbol: 'BINANCE:BTCUSDT', timeframe: '1' }, {
|
|
35
|
+
* onData: (candles) => console.log(candles.at(-1)?.close),
|
|
36
|
+
* });
|
|
37
|
+
* await watcher.stop();
|
|
38
|
+
*/
|
|
39
|
+
export declare function watchCandles(query: WatchCandlesQuery, handlers: CandleHandlers): Promise<CandleWatcher>;
|
|
40
|
+
export declare function watchCandles(query: WatchCandlesQuery, handlers: CandleHandlers, provider: MarketDataProvider): Promise<CandleSourceWorker>;
|