@mathieuc/tradingview 3.5.1 → 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 -294
- package/src/miscRequests.js +0 -607
- 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,127 @@
|
|
|
1
|
+
import { platform, arch, release } from 'node:os';
|
|
2
|
+
import { TradingViewError } from '../errors.js';
|
|
3
|
+
import { getSetCookies, request, } from './request.js';
|
|
4
|
+
/**
|
|
5
|
+
* Signs in with a username/email and password and returns the account with
|
|
6
|
+
* its session cookies. Accounts with 2FA or captcha challenges are not supported.
|
|
7
|
+
*/
|
|
8
|
+
export async function loginUser(options) {
|
|
9
|
+
const remember = options.remember ?? true;
|
|
10
|
+
const userAgent = options.userAgent ?? 'TWAPI/4.0';
|
|
11
|
+
const { data, headers } = await request('https://www.tradingview.com/accounts/signin/', {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
form: { username: options.username, password: options.password, remember: remember ? 'on' : undefined },
|
|
14
|
+
headers: {
|
|
15
|
+
referer: 'https://www.tradingview.com',
|
|
16
|
+
'user-agent': `${userAgent} (${release()}; ${platform()}; ${arch()})`,
|
|
17
|
+
},
|
|
18
|
+
}, options);
|
|
19
|
+
if (!data || typeof data !== 'object') {
|
|
20
|
+
throw new TradingViewError('AUTH_ERROR', 'Unexpected sign-in response');
|
|
21
|
+
}
|
|
22
|
+
if (data.error)
|
|
23
|
+
throw new TradingViewError('AUTH_ERROR', String(data.error), { details: data });
|
|
24
|
+
if (!data.user)
|
|
25
|
+
throw new TradingViewError('AUTH_ERROR', 'Sign-in response has no user', { details: data });
|
|
26
|
+
return userFromLogin(data.user, headers);
|
|
27
|
+
}
|
|
28
|
+
function cookieValue(headers, name) {
|
|
29
|
+
for (const line of getSetCookies(headers)) {
|
|
30
|
+
const found = new RegExp(`(?:^|[;\\s])${name}=([^;]*)`).exec(line);
|
|
31
|
+
if (found)
|
|
32
|
+
return found[1];
|
|
33
|
+
}
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
function textOrEmpty(value) {
|
|
37
|
+
return String(value ?? '');
|
|
38
|
+
}
|
|
39
|
+
function numberOrZero(value) {
|
|
40
|
+
return Number(value ?? 0);
|
|
41
|
+
}
|
|
42
|
+
function userFromLogin(user, headers) {
|
|
43
|
+
return {
|
|
44
|
+
id: numberOrZero(user.id),
|
|
45
|
+
username: user.username,
|
|
46
|
+
firstName: textOrEmpty(user.first_name),
|
|
47
|
+
lastName: textOrEmpty(user.last_name),
|
|
48
|
+
reputation: numberOrZero(user.reputation),
|
|
49
|
+
following: numberOrZero(user.following),
|
|
50
|
+
followers: numberOrZero(user.followers),
|
|
51
|
+
notifications: {
|
|
52
|
+
user: numberOrZero(user.notification_count?.user),
|
|
53
|
+
following: numberOrZero(user.notification_count?.following),
|
|
54
|
+
},
|
|
55
|
+
session: cookieValue(headers, 'sessionid'),
|
|
56
|
+
signature: cookieValue(headers, 'sessionid_sign'),
|
|
57
|
+
sessionHash: textOrEmpty(user.session_hash),
|
|
58
|
+
privateChannel: textOrEmpty(user.private_channel),
|
|
59
|
+
authToken: textOrEmpty(user.auth_token),
|
|
60
|
+
joinDate: new Date(user.date_joined),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function match(page, pattern) {
|
|
64
|
+
return pattern.exec(page)?.[1];
|
|
65
|
+
}
|
|
66
|
+
function pageNumber(page, pattern) {
|
|
67
|
+
return parseFloat(match(page, pattern) ?? '0') || 0;
|
|
68
|
+
}
|
|
69
|
+
function pageText(page, pattern) {
|
|
70
|
+
return match(page, pattern) ?? '';
|
|
71
|
+
}
|
|
72
|
+
function trustedAccountLocation(location) {
|
|
73
|
+
let url;
|
|
74
|
+
try {
|
|
75
|
+
url = new URL(location);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new TradingViewError('AUTH_ERROR', 'Invalid account page URL');
|
|
79
|
+
}
|
|
80
|
+
if (url.protocol !== 'https:' || !/(^|\.)tradingview\.com$/i.test(url.hostname)) {
|
|
81
|
+
throw new TradingViewError('AUTH_ERROR', 'Account page redirects outside TradingView');
|
|
82
|
+
}
|
|
83
|
+
return url.toString();
|
|
84
|
+
}
|
|
85
|
+
/** Parses the account embedded in a TradingView HTML page. */
|
|
86
|
+
export function parseUserPage(page, credentials) {
|
|
87
|
+
return {
|
|
88
|
+
id: pageNumber(page, /"id":([0-9]{1,10}),/),
|
|
89
|
+
username: pageText(page, /"username":"(.*?)"/),
|
|
90
|
+
firstName: pageText(page, /"first_name":"(.*?)"/),
|
|
91
|
+
lastName: pageText(page, /"last_name":"(.*?)"/),
|
|
92
|
+
reputation: pageNumber(page, /"reputation":(.*?),/),
|
|
93
|
+
following: pageNumber(page, /,"following":([0-9]*?),/),
|
|
94
|
+
followers: pageNumber(page, /,"followers":([0-9]*?),/),
|
|
95
|
+
notifications: {
|
|
96
|
+
following: pageNumber(page, /"notification_count":\{"following":([0-9]*),/),
|
|
97
|
+
user: pageNumber(page, /"notification_count":\{"following":[0-9]*,"user":([0-9]*)/),
|
|
98
|
+
},
|
|
99
|
+
session: credentials.session,
|
|
100
|
+
signature: credentials.signature ?? '',
|
|
101
|
+
sessionHash: pageText(page, /"session_hash":"(.*?)"/),
|
|
102
|
+
privateChannel: pageText(page, /"private_channel":"(.*?)"/),
|
|
103
|
+
authToken: pageText(page, /"auth_token":"(.*?)"/),
|
|
104
|
+
joinDate: new Date(pageText(page, /"date_joined":"(.*?)"/) || 0),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Loads the account behind `sessionid` (and `sessionid_sign`) cookies. */
|
|
108
|
+
export async function getUser(credentials, options = {}) {
|
|
109
|
+
if (!credentials?.session)
|
|
110
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'A session cookie is required');
|
|
111
|
+
const maxRedirects = options.maxRedirects ?? 5;
|
|
112
|
+
let location = trustedAccountLocation(options.location ?? 'https://www.tradingview.com/chart/');
|
|
113
|
+
for (let redirects = 0;; redirects += 1) {
|
|
114
|
+
const { status, text, headers } = await request(location, { credentials, redirect: 'manual' }, options);
|
|
115
|
+
if (text.includes('auth_token'))
|
|
116
|
+
return parseUserPage(text, credentials);
|
|
117
|
+
const next = headers.get('location');
|
|
118
|
+
const resolved = status >= 300 && status < 400 && next ? new URL(next, location).toString() : undefined;
|
|
119
|
+
if (!resolved || resolved === location) {
|
|
120
|
+
throw new TradingViewError('AUTH_ERROR', 'Wrong or expired sessionid/signature');
|
|
121
|
+
}
|
|
122
|
+
if (redirects >= maxRedirects) {
|
|
123
|
+
throw new TradingViewError('AUTH_ERROR', 'Too many redirects (possible WAF or geo-restriction)');
|
|
124
|
+
}
|
|
125
|
+
location = trustedAccountLocation(resolved);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { AuthHttpOptions, Credentials, HttpOptions } from './request.js';
|
|
2
|
+
export { getUser, loginUser } from './account.js';
|
|
3
|
+
export type { GetUserOptions, LoginOptions, User } from './account.js';
|
|
4
|
+
export { getTechnicalAnalysis, searchMarkets } from './market.js';
|
|
5
|
+
export type { MarketSearchResult, MarketType, SearchMarketsOptions, TechnicalAnalysis, TechnicalAnalysisPeriod, TechnicalRating, } from './market.js';
|
|
6
|
+
export { clearIndicatorCache, getIndicator, getPrivateIndicators, parseIndicatorDefinition, searchIndicators, } from './indicators.js';
|
|
7
|
+
export type { GetIndicatorOptions, IndicatorAccess, IndicatorSearchResult } from './indicators.js';
|
|
8
|
+
export { getChartToken, getDrawings } from './layouts.js';
|
|
9
|
+
export type { Drawing, DrawingPoint, GetDrawingsOptions, LayoutOptions, } from './layouts.js';
|
|
10
|
+
export { PinePermissionManager } from './pine-permissions.js';
|
|
11
|
+
export type { AuthorizedUser, AuthorizedUserOrder, PinePermissionOptions } from './pine-permissions.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { getUser, loginUser } from './account.js';
|
|
2
|
+
export { getTechnicalAnalysis, searchMarkets } from './market.js';
|
|
3
|
+
export { clearIndicatorCache, getIndicator, getPrivateIndicators, parseIndicatorDefinition, searchIndicators, } from './indicators.js';
|
|
4
|
+
export { getChartToken, getDrawings } from './layouts.js';
|
|
5
|
+
export { PinePermissionManager } from './pine-permissions.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PineIndicator } from '../indicators/pine-indicator.js';
|
|
2
|
+
import { type AuthHttpOptions, type Credentials, type HttpOptions } from './request.js';
|
|
3
|
+
export type IndicatorAccess = 'open_source' | 'closed_source' | 'invite_only' | 'private' | 'other';
|
|
4
|
+
/** An indicator returned by `searchIndicators` or `getPrivateIndicators`. */
|
|
5
|
+
export interface IndicatorSearchResult {
|
|
6
|
+
/** Script ID to pass to `getIndicator`. */
|
|
7
|
+
id: string;
|
|
8
|
+
version: string;
|
|
9
|
+
name: string;
|
|
10
|
+
author: {
|
|
11
|
+
id: number;
|
|
12
|
+
username: string;
|
|
13
|
+
};
|
|
14
|
+
/** Chart image ID: `https://www.tradingview.com/i/<image>`. */
|
|
15
|
+
image: string;
|
|
16
|
+
/** Pine source, when public. */
|
|
17
|
+
source: string;
|
|
18
|
+
type: 'study' | 'strategy' | (string & {});
|
|
19
|
+
access: IndicatorAccess;
|
|
20
|
+
}
|
|
21
|
+
/** Clears the in-memory list of built-in indicators used by `searchIndicators`. */
|
|
22
|
+
export declare function clearIndicatorCache(): void;
|
|
23
|
+
/** Searches built-in and community indicators. */
|
|
24
|
+
export declare function searchIndicators(query?: string, options?: HttpOptions): Promise<IndicatorSearchResult[]>;
|
|
25
|
+
export interface GetIndicatorOptions extends AuthHttpOptions {
|
|
26
|
+
/** Script version. Default: `last`. */
|
|
27
|
+
version?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Converts a `pine-facade/translate` result into a `PineIndicator`. */
|
|
30
|
+
export declare function parseIndicatorDefinition(result: any, id: string, version: string): PineIndicator;
|
|
31
|
+
/**
|
|
32
|
+
* Loads an indicator definition (`STD;RSI`, `PUB;xxxx`, `USER;xxxx`...).
|
|
33
|
+
* Private and invite-only scripts need credentials with access.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getIndicator(id: string, options?: GetIndicatorOptions): Promise<PineIndicator>;
|
|
36
|
+
/** Lists the private (saved) indicators of an account. */
|
|
37
|
+
export declare function getPrivateIndicators(credentials: Credentials, options?: HttpOptions): Promise<IndicatorSearchResult[]>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
import { PineIndicator } from '../indicators/pine-indicator.js';
|
|
3
|
+
import { request, } from './request.js';
|
|
4
|
+
let builtInCache;
|
|
5
|
+
async function loadBuiltIns(options) {
|
|
6
|
+
const lists = await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (filter) => {
|
|
7
|
+
const { data } = await request('https://pine-facade.tradingview.com/pine-facade/list', { query: { filter } }, options);
|
|
8
|
+
return Array.isArray(data) ? data : [];
|
|
9
|
+
}));
|
|
10
|
+
return lists.flat();
|
|
11
|
+
}
|
|
12
|
+
/** Clears the in-memory list of built-in indicators used by `searchIndicators`. */
|
|
13
|
+
export function clearIndicatorCache() {
|
|
14
|
+
builtInCache = undefined;
|
|
15
|
+
}
|
|
16
|
+
const normalise = (value = '') => value.toUpperCase().replace(/[^A-Z]/g, '');
|
|
17
|
+
/** Searches built-in and community indicators. */
|
|
18
|
+
export async function searchIndicators(query = '', options = {}) {
|
|
19
|
+
// Custom fetch implementations (tests, proxies) must not share the process-wide cache.
|
|
20
|
+
const builtInsPromise = options.fetch ? loadBuiltIns(options) : (builtInCache ??= loadBuiltIns(options));
|
|
21
|
+
const [builtIns, community] = await Promise.all([
|
|
22
|
+
builtInsPromise.catch((error) => {
|
|
23
|
+
if (!options.fetch)
|
|
24
|
+
builtInCache = undefined;
|
|
25
|
+
throw error;
|
|
26
|
+
}),
|
|
27
|
+
request('https://www.tradingview.com/pubscripts-suggest-json', { query: { search: query } }, options),
|
|
28
|
+
]);
|
|
29
|
+
const needle = normalise(query);
|
|
30
|
+
const kind = (ind) => ind.extra?.kind ?? 'study';
|
|
31
|
+
return [
|
|
32
|
+
...builtIns
|
|
33
|
+
.filter((ind) => normalise(ind.scriptName).includes(needle)
|
|
34
|
+
|| normalise(ind.extra?.shortDescription).includes(needle))
|
|
35
|
+
.map((ind) => ({
|
|
36
|
+
id: ind.scriptIdPart,
|
|
37
|
+
version: String(ind.version),
|
|
38
|
+
name: ind.scriptName,
|
|
39
|
+
author: { id: Number(ind.userId ?? -1), username: '@TRADINGVIEW@' },
|
|
40
|
+
image: '',
|
|
41
|
+
source: '',
|
|
42
|
+
type: kind(ind),
|
|
43
|
+
access: 'closed_source',
|
|
44
|
+
})),
|
|
45
|
+
...(community.data?.results ?? []).map((ind) => ({
|
|
46
|
+
id: ind.scriptIdPart,
|
|
47
|
+
version: String(ind.version),
|
|
48
|
+
name: ind.scriptName,
|
|
49
|
+
author: { id: Number(ind.author?.id ?? -1), username: ind.author?.username ?? '' },
|
|
50
|
+
image: ind.imageUrl ?? '',
|
|
51
|
+
source: ind.scriptSource ?? '',
|
|
52
|
+
type: kind(ind),
|
|
53
|
+
access: ['open_source', 'closed_source', 'invite_only'][ind.access - 1] ?? 'other',
|
|
54
|
+
})),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
const sanitise = (value) => value.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
|
|
58
|
+
function parseInputs(items) {
|
|
59
|
+
const inputs = {};
|
|
60
|
+
for (const input of items) {
|
|
61
|
+
if (['text', 'pineId', 'pineVersion'].includes(input.id))
|
|
62
|
+
continue;
|
|
63
|
+
const inlineName = sanitise(String(input.name ?? input.id));
|
|
64
|
+
inputs[input.id] = {
|
|
65
|
+
name: input.name,
|
|
66
|
+
inline: input.inline || inlineName,
|
|
67
|
+
internalID: input.internalID || inlineName,
|
|
68
|
+
tooltip: input.tooltip,
|
|
69
|
+
type: input.type,
|
|
70
|
+
value: input.defval,
|
|
71
|
+
isHidden: !!input.isHidden,
|
|
72
|
+
isFake: !!input.isFake,
|
|
73
|
+
...(input.options ? { options: input.options } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return inputs;
|
|
77
|
+
}
|
|
78
|
+
function parsePlots(styles, items) {
|
|
79
|
+
const plots = {};
|
|
80
|
+
for (const [plotId, style] of Object.entries(styles)) {
|
|
81
|
+
const title = sanitise(String(style.title ?? plotId));
|
|
82
|
+
const titles = Object.values(plots);
|
|
83
|
+
if (titles.includes(title)) {
|
|
84
|
+
let i = 2;
|
|
85
|
+
while (titles.includes(`${title}_${i}`))
|
|
86
|
+
i += 1;
|
|
87
|
+
plots[plotId] = `${title}_${i}`;
|
|
88
|
+
}
|
|
89
|
+
else
|
|
90
|
+
plots[plotId] = title;
|
|
91
|
+
}
|
|
92
|
+
for (const plot of items) {
|
|
93
|
+
if (!plot.target)
|
|
94
|
+
continue;
|
|
95
|
+
plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}`;
|
|
96
|
+
}
|
|
97
|
+
return plots;
|
|
98
|
+
}
|
|
99
|
+
/** Converts a `pine-facade/translate` result into a `PineIndicator`. */
|
|
100
|
+
export function parseIndicatorDefinition(result, id, version) {
|
|
101
|
+
const meta = result.metaInfo;
|
|
102
|
+
return new PineIndicator({
|
|
103
|
+
id: meta.scriptIdPart || id,
|
|
104
|
+
version: meta.pine?.version || version,
|
|
105
|
+
description: meta.description,
|
|
106
|
+
shortDescription: meta.shortDescription,
|
|
107
|
+
inputs: parseInputs(meta.inputs),
|
|
108
|
+
plots: parsePlots(meta.styles ?? {}, meta.plots ?? []),
|
|
109
|
+
script: result.ilTemplate,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Loads an indicator definition (`STD;RSI`, `PUB;xxxx`, `USER;xxxx`...).
|
|
114
|
+
* Private and invite-only scripts need credentials with access.
|
|
115
|
+
*/
|
|
116
|
+
export async function getIndicator(id, options = {}) {
|
|
117
|
+
const version = options.version ?? 'last';
|
|
118
|
+
const encodedId = id.replace(/ |%/g, '%25');
|
|
119
|
+
const { data } = await request(`https://pine-facade.tradingview.com/pine-facade/translate/${encodedId}/${version}`, { credentials: options.credentials }, options);
|
|
120
|
+
if (!data?.success || !data.result?.metaInfo?.inputs) {
|
|
121
|
+
throw new TradingViewError('NOT_FOUND', `Inexistent or unsupported indicator: "${data?.reason}"`, { details: data });
|
|
122
|
+
}
|
|
123
|
+
return parseIndicatorDefinition(data.result, encodedId, version);
|
|
124
|
+
}
|
|
125
|
+
/** Lists the private (saved) indicators of an account. */
|
|
126
|
+
export async function getPrivateIndicators(credentials, options = {}) {
|
|
127
|
+
const { data } = await request('https://pine-facade.tradingview.com/pine-facade/list', {
|
|
128
|
+
query: { filter: 'saved' },
|
|
129
|
+
credentials,
|
|
130
|
+
}, options);
|
|
131
|
+
if (!Array.isArray(data)) {
|
|
132
|
+
throw new TradingViewError('AUTH_ERROR', 'Unable to list private indicators (check credentials)', { details: data });
|
|
133
|
+
}
|
|
134
|
+
return data.map((ind) => ({
|
|
135
|
+
id: ind.scriptIdPart,
|
|
136
|
+
version: String(ind.version),
|
|
137
|
+
name: ind.scriptName,
|
|
138
|
+
author: { id: -1, username: '@ME@' },
|
|
139
|
+
image: ind.imageUrl ?? '',
|
|
140
|
+
source: ind.scriptSource ?? '',
|
|
141
|
+
type: ind.extra?.kind ?? 'study',
|
|
142
|
+
access: 'private',
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type AuthHttpOptions } from './request.js';
|
|
2
|
+
export interface LayoutOptions extends AuthHttpOptions {
|
|
3
|
+
/** Account ID (`User.id`), required with credentials for private layouts. */
|
|
4
|
+
userId?: number;
|
|
5
|
+
}
|
|
6
|
+
/** Gets a chart-storage token for a layout (the ID in `tradingview.com/chart/<ID>/`). */
|
|
7
|
+
export declare function getChartToken(layoutId: string, options?: LayoutOptions): Promise<string>;
|
|
8
|
+
export interface DrawingPoint {
|
|
9
|
+
time_t: number;
|
|
10
|
+
price: number;
|
|
11
|
+
offset: number;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** A user drawing stored in a layout. State properties are merged at the top level. */
|
|
15
|
+
export interface Drawing {
|
|
16
|
+
id: string;
|
|
17
|
+
symbol: string;
|
|
18
|
+
ownerSource: string;
|
|
19
|
+
serverUpdateTime: string;
|
|
20
|
+
currencyId: string;
|
|
21
|
+
unitId: unknown;
|
|
22
|
+
type: string;
|
|
23
|
+
points: DrawingPoint[];
|
|
24
|
+
zorder: number;
|
|
25
|
+
linkKey: string;
|
|
26
|
+
state: Record<string, any>;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface GetDrawingsOptions extends LayoutOptions {
|
|
30
|
+
/** Only returns drawings for this symbol. */
|
|
31
|
+
symbol?: string;
|
|
32
|
+
/** Chart ID inside the layout. Default: `_shared`. */
|
|
33
|
+
chartId?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Lists the drawings of a layout. */
|
|
36
|
+
export declare function getDrawings(layoutId: string, options?: GetDrawingsOptions): Promise<Drawing[]>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
import { request } from './request.js';
|
|
3
|
+
/** Gets a chart-storage token for a layout (the ID in `tradingview.com/chart/<ID>/`). */
|
|
4
|
+
export async function getChartToken(layoutId, options = {}) {
|
|
5
|
+
const authenticated = Boolean(options.credentials?.session && options.userId);
|
|
6
|
+
const { data } = await request('https://www.tradingview.com/chart-token', {
|
|
7
|
+
query: { image_url: layoutId, user_id: authenticated ? options.userId : -1 },
|
|
8
|
+
credentials: authenticated ? options.credentials : undefined,
|
|
9
|
+
}, options);
|
|
10
|
+
if (!data?.token)
|
|
11
|
+
throw new TradingViewError('NOT_FOUND', 'Wrong layout or credentials', { details: data });
|
|
12
|
+
return data.token;
|
|
13
|
+
}
|
|
14
|
+
/** Lists the drawings of a layout. */
|
|
15
|
+
export async function getDrawings(layoutId, options = {}) {
|
|
16
|
+
const token = await getChartToken(layoutId, options);
|
|
17
|
+
const { data } = await request(`https://charts-storage.tradingview.com/charts-storage/get/layout/${layoutId}/sources`, {
|
|
18
|
+
query: { chart_id: options.chartId ?? '_shared', jwt: token, symbol: options.symbol ?? '' },
|
|
19
|
+
}, options);
|
|
20
|
+
if (!data?.payload) {
|
|
21
|
+
throw new TradingViewError('NOT_FOUND', 'Wrong layout, user credentials, or chart id', { details: data });
|
|
22
|
+
}
|
|
23
|
+
return Object.values(data.payload.sources ?? {}).map((drawing) => ({ ...drawing, ...drawing.state }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type HttpOptions } from './request.js';
|
|
2
|
+
export type MarketType = 'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic' | 'bond' | 'fund' | (string & {});
|
|
3
|
+
/** One market returned by `searchMarkets`. */
|
|
4
|
+
export interface MarketSearchResult {
|
|
5
|
+
/** Exchange-qualified symbol, usable everywhere in this library (`BINANCE:BTCEUR`). */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Short exchange name (`BINANCE`). */
|
|
8
|
+
exchange: string;
|
|
9
|
+
/** Exchange name as sent by TradingView. */
|
|
10
|
+
fullExchange: string;
|
|
11
|
+
symbol: string;
|
|
12
|
+
description: string;
|
|
13
|
+
type: string;
|
|
14
|
+
currency?: string;
|
|
15
|
+
country?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SearchMarketsOptions extends HttpOptions {
|
|
18
|
+
/** Restricts results to a market type. */
|
|
19
|
+
type?: MarketType;
|
|
20
|
+
/** Pagination offset. */
|
|
21
|
+
offset?: number;
|
|
22
|
+
/** Restricts results to an exchange. `searchMarkets('BINANCE:BTC')` sets it too. */
|
|
23
|
+
exchange?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Searches markets by keywords, `EXCHANGE:` prefix or `EXCHANGE:SYMBOL`. */
|
|
26
|
+
export declare function searchMarkets(query: string, options?: SearchMarketsOptions): Promise<MarketSearchResult[]>;
|
|
27
|
+
/**
|
|
28
|
+
* TradingView technical ratings, from -1 (strong sell) to 1 (strong buy).
|
|
29
|
+
* `All` is the summary, `MA` the moving averages and `Other` the oscillators.
|
|
30
|
+
*/
|
|
31
|
+
export interface TechnicalRating {
|
|
32
|
+
Other: number;
|
|
33
|
+
All: number;
|
|
34
|
+
MA: number;
|
|
35
|
+
}
|
|
36
|
+
export type TechnicalAnalysisPeriod = '1' | '5' | '15' | '60' | '240' | '1D' | '1W' | '1M';
|
|
37
|
+
export type TechnicalAnalysis = Record<TechnicalAnalysisPeriod, TechnicalRating>;
|
|
38
|
+
/**
|
|
39
|
+
* Gets technical ratings for every period from the TradingView scanner.
|
|
40
|
+
* Returns `null` when the scanner has no data for the symbol.
|
|
41
|
+
*
|
|
42
|
+
* Values are scaled like the historical API: `round(raw * 1000) / 500`,
|
|
43
|
+
* which maps the scanner range [-1, 1] to [-2, 2].
|
|
44
|
+
*/
|
|
45
|
+
export declare function getTechnicalAnalysis(symbol: string, options?: HttpOptions): Promise<TechnicalAnalysis | null>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
import { request } from './request.js';
|
|
3
|
+
const ORIGIN = { origin: 'https://www.tradingview.com' };
|
|
4
|
+
/** Searches markets by keywords, `EXCHANGE:` prefix or `EXCHANGE:SYMBOL`. */
|
|
5
|
+
export async function searchMarkets(query, options = {}) {
|
|
6
|
+
const parts = query.toUpperCase().split(':');
|
|
7
|
+
const text = parts.length === 2 ? parts[1] : query.toUpperCase();
|
|
8
|
+
const exchange = options.exchange ?? (parts.length === 2 ? parts[0] : undefined);
|
|
9
|
+
const { data, status } = await request('https://symbol-search.tradingview.com/symbol_search/v3/', {
|
|
10
|
+
query: {
|
|
11
|
+
text, exchange, search_type: options.type ?? '', start: options.offset ?? 0,
|
|
12
|
+
},
|
|
13
|
+
headers: ORIGIN,
|
|
14
|
+
}, options);
|
|
15
|
+
if (!data || !Array.isArray(data.symbols)) {
|
|
16
|
+
throw new TradingViewError('HTTP_ERROR', `Unexpected market search response (HTTP ${status})`, { details: data });
|
|
17
|
+
}
|
|
18
|
+
return data.symbols.map((s) => {
|
|
19
|
+
const shortExchange = String(s.exchange ?? '').split(' ')[0];
|
|
20
|
+
const symbol = String(s.symbol ?? '').replace(/<\/?em>/g, '');
|
|
21
|
+
return {
|
|
22
|
+
id: s.prefix ? `${s.prefix}:${symbol}` : `${shortExchange.toUpperCase()}:${symbol}`,
|
|
23
|
+
exchange: shortExchange,
|
|
24
|
+
fullExchange: s.exchange,
|
|
25
|
+
symbol,
|
|
26
|
+
description: String(s.description ?? '').replace(/<\/?em>/g, ''),
|
|
27
|
+
type: s.type,
|
|
28
|
+
currency: s.currency_code,
|
|
29
|
+
country: s.country,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const TA_PERIODS = ['1', '5', '15', '60', '240', '1D', '1W', '1M'];
|
|
34
|
+
const TA_INDICATORS = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
|
|
35
|
+
/**
|
|
36
|
+
* Gets technical ratings for every period from the TradingView scanner.
|
|
37
|
+
* Returns `null` when the scanner has no data for the symbol.
|
|
38
|
+
*
|
|
39
|
+
* Values are scaled like the historical API: `round(raw * 1000) / 500`,
|
|
40
|
+
* which maps the scanner range [-1, 1] to [-2, 2].
|
|
41
|
+
*/
|
|
42
|
+
export async function getTechnicalAnalysis(symbol, options = {}) {
|
|
43
|
+
const columns = TA_PERIODS.flatMap((period) => TA_INDICATORS.map((name) => (period === '1D' ? name : `${name}|${period}`)));
|
|
44
|
+
const { data } = await request('https://scanner.tradingview.com/global/scan', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
json: { symbols: { tickers: [symbol] }, columns },
|
|
47
|
+
}, options);
|
|
48
|
+
const values = data?.data?.[0]?.d;
|
|
49
|
+
if (!values)
|
|
50
|
+
return null;
|
|
51
|
+
const result = {};
|
|
52
|
+
values.forEach((value, index) => {
|
|
53
|
+
const [name, period = '1D'] = columns[index].split('|');
|
|
54
|
+
const key = name.split('.').pop();
|
|
55
|
+
result[period] ??= {};
|
|
56
|
+
result[period][key] = Math.round(Number(value) * 1000) / 500;
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Credentials, type HttpOptions } from './request.js';
|
|
2
|
+
/** A user allowed to use an invite-only script. */
|
|
3
|
+
export interface AuthorizedUser {
|
|
4
|
+
id: number;
|
|
5
|
+
username: string;
|
|
6
|
+
userpic: string;
|
|
7
|
+
/** ISO date, or null when the access does not expire. */
|
|
8
|
+
expiration: string | null;
|
|
9
|
+
created: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export type AuthorizedUserOrder = 'user__username' | '-user__username' | 'created' | '-created' | 'expiration,user__username' | '-expiration,user__username';
|
|
13
|
+
export interface PinePermissionOptions extends HttpOptions {
|
|
14
|
+
credentials: Credentials;
|
|
15
|
+
}
|
|
16
|
+
/** Manages who can use an invite-only Pine script you own. */
|
|
17
|
+
export declare class PinePermissionManager {
|
|
18
|
+
#private;
|
|
19
|
+
readonly pineId: string;
|
|
20
|
+
constructor(pineId: string, options: PinePermissionOptions);
|
|
21
|
+
/** Lists authorized users. */
|
|
22
|
+
getUsers(limit?: number, order?: AuthorizedUserOrder): Promise<AuthorizedUser[]>;
|
|
23
|
+
/** Authorizes a user, optionally until a date. */
|
|
24
|
+
addUser(username: string, expiration?: Date): Promise<'ok' | 'exists' | (string & {})>;
|
|
25
|
+
/** Changes (or removes, without date) the expiration of a user's access. */
|
|
26
|
+
modifyExpiration(username: string, expiration?: Date): Promise<'ok' | (string & {})>;
|
|
27
|
+
/** Revokes a user's access. */
|
|
28
|
+
removeUser(username: string): Promise<'ok' | (string & {})>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
import { request } from './request.js';
|
|
3
|
+
/** Manages who can use an invite-only Pine script you own. */
|
|
4
|
+
export class PinePermissionManager {
|
|
5
|
+
pineId;
|
|
6
|
+
#credentials;
|
|
7
|
+
#options;
|
|
8
|
+
constructor(pineId, options) {
|
|
9
|
+
if (!pineId)
|
|
10
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'A Pine ID is required');
|
|
11
|
+
if (!options?.credentials?.session)
|
|
12
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'A session cookie is required');
|
|
13
|
+
if (!options.credentials.signature)
|
|
14
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'A session signature is required');
|
|
15
|
+
this.pineId = pineId;
|
|
16
|
+
this.#credentials = options.credentials;
|
|
17
|
+
this.#options = { fetch: options.fetch, signal: options.signal, headers: options.headers };
|
|
18
|
+
}
|
|
19
|
+
async #post(path, form, query) {
|
|
20
|
+
const { status, data } = await request(`https://www.tradingview.com/pine_perm/${path}`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
query,
|
|
23
|
+
form: { pine_id: this.pineId, ...form },
|
|
24
|
+
credentials: this.#credentials,
|
|
25
|
+
headers: { origin: 'https://www.tradingview.com' },
|
|
26
|
+
}, this.#options);
|
|
27
|
+
if (status >= 400) {
|
|
28
|
+
throw new TradingViewError(status === 404 ? 'NOT_FOUND' : 'HTTP_ERROR', data?.detail ?? 'Wrong credentials or pineId', {
|
|
29
|
+
details: data,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
/** Lists authorized users. */
|
|
35
|
+
async getUsers(limit = 10, order = '-created') {
|
|
36
|
+
const data = await this.#post('list_users/', {}, { limit, order_by: order });
|
|
37
|
+
return data?.results ?? [];
|
|
38
|
+
}
|
|
39
|
+
/** Authorizes a user, optionally until a date. */
|
|
40
|
+
async addUser(username, expiration) {
|
|
41
|
+
const data = await this.#post('add/', { username_recip: username, expiration: expiration?.toISOString() });
|
|
42
|
+
return data?.status;
|
|
43
|
+
}
|
|
44
|
+
/** Changes (or removes, without date) the expiration of a user's access. */
|
|
45
|
+
async modifyExpiration(username, expiration) {
|
|
46
|
+
const data = await this.#post('modify_user_expiration/', {
|
|
47
|
+
username_recip: username, expiration: expiration?.toISOString(),
|
|
48
|
+
});
|
|
49
|
+
return data?.status;
|
|
50
|
+
}
|
|
51
|
+
/** Revokes a user's access. */
|
|
52
|
+
async removeUser(username) {
|
|
53
|
+
const data = await this.#post('remove/', { username_recip: username });
|
|
54
|
+
return data?.status;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** TradingView account cookies. Keep them secret. */
|
|
2
|
+
export interface Credentials {
|
|
3
|
+
/** Value of the `sessionid` cookie. */
|
|
4
|
+
session: string;
|
|
5
|
+
/** Value of the `sessionid_sign` cookie. */
|
|
6
|
+
signature?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Options accepted by every HTTP function. */
|
|
9
|
+
export interface HttpOptions {
|
|
10
|
+
/** Custom `fetch` implementation (tests, proxies). Defaults to the global `fetch`. */
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
/** Cancels the request. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Extra request headers. */
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/** HTTP options for endpoints that can use an account. */
|
|
18
|
+
export interface AuthHttpOptions extends HttpOptions {
|
|
19
|
+
credentials?: Credentials;
|
|
20
|
+
}
|
|
21
|
+
export declare const DEFAULT_USER_AGENT: string;
|
|
22
|
+
/** Builds the `cookie` header value for credentials. */
|
|
23
|
+
export declare function authCookies(credentials?: Credentials | null): string;
|
|
24
|
+
export interface RequestInit {
|
|
25
|
+
method?: 'GET' | 'POST';
|
|
26
|
+
query?: Record<string, string | number | boolean | undefined | null>;
|
|
27
|
+
form?: Record<string, string | number | undefined | null>;
|
|
28
|
+
json?: unknown;
|
|
29
|
+
credentials?: Credentials | null;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
redirect?: 'follow' | 'manual';
|
|
32
|
+
}
|
|
33
|
+
export interface HttpResponse {
|
|
34
|
+
status: number;
|
|
35
|
+
headers: Headers;
|
|
36
|
+
url: string;
|
|
37
|
+
text: string;
|
|
38
|
+
/** Parsed JSON body, or `undefined` when the body is not JSON. */
|
|
39
|
+
data: any;
|
|
40
|
+
}
|
|
41
|
+
/** Performs a request. Like the historical client, 5xx statuses throw. */
|
|
42
|
+
export declare function request(url: string, init: RequestInit, options?: HttpOptions): Promise<HttpResponse>;
|
|
43
|
+
/** Reads every `set-cookie` header, across runtimes. */
|
|
44
|
+
export declare function getSetCookies(headers: Headers): string[];
|