@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,83 @@
|
|
|
1
|
+
import { TradingViewError, toTradingViewError } from '../errors.js';
|
|
2
|
+
export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 '
|
|
3
|
+
+ '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
|
4
|
+
/** Builds the `cookie` header value for credentials. */
|
|
5
|
+
export function authCookies(credentials) {
|
|
6
|
+
if (!credentials?.session)
|
|
7
|
+
return '';
|
|
8
|
+
if (!credentials.signature)
|
|
9
|
+
return `sessionid=${credentials.session}`;
|
|
10
|
+
return `sessionid=${credentials.session};sessionid_sign=${credentials.signature}`;
|
|
11
|
+
}
|
|
12
|
+
function requestUrl(url, query) {
|
|
13
|
+
const target = new URL(url);
|
|
14
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
15
|
+
if (value !== undefined && value !== null)
|
|
16
|
+
target.searchParams.set(key, String(value));
|
|
17
|
+
}
|
|
18
|
+
return target;
|
|
19
|
+
}
|
|
20
|
+
function requestBody(init, headers) {
|
|
21
|
+
if (init.form) {
|
|
22
|
+
const form = new URLSearchParams();
|
|
23
|
+
for (const [key, value] of Object.entries(init.form)) {
|
|
24
|
+
if (value !== undefined && value !== null)
|
|
25
|
+
form.set(key, String(value));
|
|
26
|
+
}
|
|
27
|
+
headers['content-type'] = 'application/x-www-form-urlencoded';
|
|
28
|
+
return form.toString();
|
|
29
|
+
}
|
|
30
|
+
if (init.json !== undefined) {
|
|
31
|
+
headers['content-type'] = 'application/json';
|
|
32
|
+
return JSON.stringify(init.json);
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
function responseData(text) {
|
|
37
|
+
try {
|
|
38
|
+
return text ? JSON.parse(text) : undefined;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Performs a request. Like the historical client, 5xx statuses throw. */
|
|
45
|
+
export async function request(url, init, options = {}) {
|
|
46
|
+
const target = requestUrl(url, init.query);
|
|
47
|
+
const headers = { ...options.headers, ...init.headers };
|
|
48
|
+
const cookie = authCookies(init.credentials);
|
|
49
|
+
if (cookie)
|
|
50
|
+
headers.cookie = cookie;
|
|
51
|
+
const body = requestBody(init, headers);
|
|
52
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await fetchImpl(target.toString(), {
|
|
56
|
+
method: init.method ?? (body === undefined ? 'GET' : 'POST'),
|
|
57
|
+
headers,
|
|
58
|
+
body,
|
|
59
|
+
redirect: init.redirect ?? 'follow',
|
|
60
|
+
signal: options.signal,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw toTradingViewError(error, 'HTTP_ERROR');
|
|
65
|
+
}
|
|
66
|
+
const text = await response.text();
|
|
67
|
+
if (response.status >= 500) {
|
|
68
|
+
throw new TradingViewError('HTTP_ERROR', `${target.host} answered HTTP ${response.status}`, {
|
|
69
|
+
details: { status: response.status, body: text.slice(0, 500) },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
status: response.status, headers: response.headers, url: target.toString(), text, data: responseData(text),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Reads every `set-cookie` header, across runtimes. */
|
|
77
|
+
export function getSetCookies(headers) {
|
|
78
|
+
const withGetter = headers;
|
|
79
|
+
if (typeof withGetter.getSetCookie === 'function')
|
|
80
|
+
return withGetter.getSetCookie();
|
|
81
|
+
const raw = headers.get('set-cookie');
|
|
82
|
+
return raw ? raw.split(/,(?=\s*[^;,=\s]+=)/) : [];
|
|
83
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TradingView-API v4.
|
|
3
|
+
*
|
|
4
|
+
* - High-level data API (also available from `@mathieuc/tradingview/data`):
|
|
5
|
+
* `getCandles`, `watchCandles`, `getQuote`, `watchQuotes`, `getIndicatorData`...
|
|
6
|
+
* - Low-level API: `TradingViewClient`, chart and quote sessions, studies.
|
|
7
|
+
* - HTTP API: search, technical analysis, indicators, accounts, layouts.
|
|
8
|
+
*
|
|
9
|
+
* Independent community project, not affiliated with TradingView.
|
|
10
|
+
*/
|
|
11
|
+
export * from './data/index.js';
|
|
12
|
+
export * from './client/index.js';
|
|
13
|
+
export * from './chart/index.js';
|
|
14
|
+
export * from './quote/index.js';
|
|
15
|
+
export * from './indicators/index.js';
|
|
16
|
+
export * from './http/index.js';
|
|
17
|
+
export * as protocol from './protocol/index.js';
|
|
18
|
+
export { Emitter } from './events.js';
|
|
19
|
+
export type { AnyListener, Listener, Unsubscribe } from './events.js';
|
|
20
|
+
export { TradingViewError, toTradingViewError } from './errors.js';
|
|
21
|
+
export type { TradingViewErrorCode, TradingViewErrorOptions } from './errors.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TradingView-API v4.
|
|
3
|
+
*
|
|
4
|
+
* - High-level data API (also available from `@mathieuc/tradingview/data`):
|
|
5
|
+
* `getCandles`, `watchCandles`, `getQuote`, `watchQuotes`, `getIndicatorData`...
|
|
6
|
+
* - Low-level API: `TradingViewClient`, chart and quote sessions, studies.
|
|
7
|
+
* - HTTP API: search, technical analysis, indicators, accounts, layouts.
|
|
8
|
+
*
|
|
9
|
+
* Independent community project, not affiliated with TradingView.
|
|
10
|
+
*/
|
|
11
|
+
export * from './data/index.js';
|
|
12
|
+
export * from './client/index.js';
|
|
13
|
+
export * from './chart/index.js';
|
|
14
|
+
export * from './quote/index.js';
|
|
15
|
+
export * from './indicators/index.js';
|
|
16
|
+
export * from './http/index.js';
|
|
17
|
+
export * as protocol from './protocol/index.js';
|
|
18
|
+
export { Emitter } from './events.js';
|
|
19
|
+
export { TradingViewError, toTradingViewError } from './errors.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Known built-in (non-Pine) study types. Any other type string is accepted too. */
|
|
2
|
+
export type BuiltInIndicatorType = 'Volume@tv-basicstudies-241' | 'VbPFixed@tv-basicstudies-241' | 'VbPFixed@tv-basicstudies-241!' | 'VbPFixed@tv-volumebyprice-53!' | 'VbPSessions@tv-volumebyprice-53' | 'VbPSessionsRough@tv-volumebyprice-53!' | 'VbPSessionsDetailed@tv-volumebyprice-53!' | 'VbPVisible@tv-volumebyprice-53' | (string & {});
|
|
3
|
+
export type BuiltInIndicatorOption = 'length' | 'col_prev_close' | 'rowsLayout' | 'rows' | 'volume' | 'vaVolume' | 'subscribeRealtime' | 'first_bar_time' | 'first_visible_bar_time' | 'last_bar_time' | 'last_visible_bar_time' | 'extendToRight' | 'mapRightBoundaryToBarStartTime' | 'extendPocRight' | (string & {});
|
|
4
|
+
/** A built-in TradingView study such as Volume or Volume Profile. */
|
|
5
|
+
export declare class BuiltInIndicator {
|
|
6
|
+
#private;
|
|
7
|
+
readonly type: BuiltInIndicatorType;
|
|
8
|
+
readonly options: Record<string, unknown>;
|
|
9
|
+
constructor(type: BuiltInIndicatorType, options?: Record<string, unknown>);
|
|
10
|
+
/**
|
|
11
|
+
* Changes an option. For known types, the key and value type are validated
|
|
12
|
+
* unless `force` is true.
|
|
13
|
+
*/
|
|
14
|
+
setOption(key: BuiltInIndicatorOption, value: unknown, force?: boolean): this;
|
|
15
|
+
/** Parameters sent with `create_study` / `modify_study`. */
|
|
16
|
+
toStudyInputs(): Record<string, unknown>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
function defaultOptions(type) {
|
|
3
|
+
const now = Date.now();
|
|
4
|
+
const profile = {
|
|
5
|
+
rowsLayout: 'Number Of Rows', rows: 24, volume: 'Up/Down', vaVolume: 70,
|
|
6
|
+
};
|
|
7
|
+
switch (type) {
|
|
8
|
+
case 'Volume@tv-basicstudies-241':
|
|
9
|
+
return { length: 20, col_prev_close: false };
|
|
10
|
+
case 'VbPFixed@tv-basicstudies-241':
|
|
11
|
+
return {
|
|
12
|
+
...profile,
|
|
13
|
+
subscribeRealtime: false,
|
|
14
|
+
first_bar_time: NaN,
|
|
15
|
+
last_bar_time: now,
|
|
16
|
+
extendToRight: false,
|
|
17
|
+
mapRightBoundaryToBarStartTime: true,
|
|
18
|
+
};
|
|
19
|
+
case 'VbPFixed@tv-basicstudies-241!':
|
|
20
|
+
case 'VbPFixed@tv-volumebyprice-53!':
|
|
21
|
+
return {
|
|
22
|
+
...profile, subscribeRealtime: false, first_bar_time: NaN, last_bar_time: now,
|
|
23
|
+
};
|
|
24
|
+
case 'VbPSessions@tv-volumebyprice-53':
|
|
25
|
+
return { ...profile, extendPocRight: false };
|
|
26
|
+
case 'VbPSessionsRough@tv-volumebyprice-53!':
|
|
27
|
+
return { volume: 'Up/Down', vaVolume: 70 };
|
|
28
|
+
case 'VbPSessionsDetailed@tv-volumebyprice-53!':
|
|
29
|
+
return {
|
|
30
|
+
volume: 'Up/Down', vaVolume: 70, subscribeRealtime: false, first_visible_bar_time: NaN, last_visible_bar_time: now,
|
|
31
|
+
};
|
|
32
|
+
case 'VbPVisible@tv-volumebyprice-53':
|
|
33
|
+
return {
|
|
34
|
+
...profile, subscribeRealtime: false, first_visible_bar_time: NaN, last_visible_bar_time: now,
|
|
35
|
+
};
|
|
36
|
+
default:
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** A built-in TradingView study such as Volume or Volume Profile. */
|
|
41
|
+
export class BuiltInIndicator {
|
|
42
|
+
type;
|
|
43
|
+
options;
|
|
44
|
+
#defaults;
|
|
45
|
+
constructor(type, options = {}) {
|
|
46
|
+
if (!type)
|
|
47
|
+
throw new TradingViewError('INVALID_ARGUMENT', 'A built-in indicator type is required');
|
|
48
|
+
this.type = type;
|
|
49
|
+
this.#defaults = defaultOptions(type);
|
|
50
|
+
this.options = { ...this.#defaults };
|
|
51
|
+
for (const [key, value] of Object.entries(options))
|
|
52
|
+
this.setOption(key, value);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Changes an option. For known types, the key and value type are validated
|
|
56
|
+
* unless `force` is true.
|
|
57
|
+
*/
|
|
58
|
+
setOption(key, value, force = false) {
|
|
59
|
+
if (!force && this.#defaults) {
|
|
60
|
+
if (!(key in this.#defaults)) {
|
|
61
|
+
throw new TradingViewError('INVALID_ARGUMENT', `Option '${key}' is not allowed for '${this.type}'`);
|
|
62
|
+
}
|
|
63
|
+
const expected = typeof this.#defaults[key];
|
|
64
|
+
if (typeof value !== expected) {
|
|
65
|
+
throw new TradingViewError('INVALID_ARGUMENT', `Option '${key}' must be a ${expected}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.options[key] = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
/** Parameters sent with `create_study` / `modify_study`. */
|
|
72
|
+
toStudyInputs() {
|
|
73
|
+
return { ...this.options };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PineIndicator } from './pine-indicator.js';
|
|
2
|
+
export type { PineIndicatorDefinition, PineInput, PineInputType, PineStudyType, } from './pine-indicator.js';
|
|
3
|
+
export { BuiltInIndicator } from './builtin-indicator.js';
|
|
4
|
+
export type { BuiltInIndicatorOption, BuiltInIndicatorType } from './builtin-indicator.js';
|
|
5
|
+
import type { BuiltInIndicator } from './builtin-indicator.js';
|
|
6
|
+
import type { PineIndicator } from './pine-indicator.js';
|
|
7
|
+
/** Anything that can run as a study on a chart. */
|
|
8
|
+
export type Indicator = PineIndicator | BuiltInIndicator;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Pine input types sent by TradingView. */
|
|
2
|
+
export type PineInputType = 'text' | 'source' | 'integer' | 'float' | 'resolution' | 'bool' | 'color' | 'session' | 'symbol' | 'time' | 'price' | 'textarea' | (string & {});
|
|
3
|
+
export interface PineInput {
|
|
4
|
+
/** Display name. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Name usable with `setInput` (inline name or sanitised display name). */
|
|
7
|
+
inline: string;
|
|
8
|
+
/** Internal ID, when the script defines one. */
|
|
9
|
+
internalID?: string;
|
|
10
|
+
tooltip?: string;
|
|
11
|
+
type: PineInputType;
|
|
12
|
+
value: unknown;
|
|
13
|
+
isHidden: boolean;
|
|
14
|
+
isFake: boolean;
|
|
15
|
+
/** Allowed values for select inputs. */
|
|
16
|
+
options?: unknown[];
|
|
17
|
+
}
|
|
18
|
+
/** Study type used to create the indicator on a chart. */
|
|
19
|
+
export type PineStudyType = 'Script@tv-scripting-101!' | 'StrategyScript@tv-scripting-101!';
|
|
20
|
+
export interface PineIndicatorDefinition {
|
|
21
|
+
/** Script ID, like `STD;RSI`, `PUB;xxxx` or `USER;xxxx`. */
|
|
22
|
+
id: string;
|
|
23
|
+
version: string;
|
|
24
|
+
description: string;
|
|
25
|
+
shortDescription: string;
|
|
26
|
+
/** Inputs indexed by input ID (`in_0`, `in_1`...). */
|
|
27
|
+
inputs: Record<string, PineInput>;
|
|
28
|
+
/** Plot names indexed by plot ID (`plot_0`...). */
|
|
29
|
+
plots: Record<string, string>;
|
|
30
|
+
/** Compiled script (IL template) sent to the chart. */
|
|
31
|
+
script: string;
|
|
32
|
+
/** Study type. Defaults to `Script@tv-scripting-101!`. */
|
|
33
|
+
type?: PineStudyType;
|
|
34
|
+
}
|
|
35
|
+
/** A Pine script (built-in, public, invite-only or private) ready to run on a chart. */
|
|
36
|
+
export declare class PineIndicator {
|
|
37
|
+
#private;
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly version: string;
|
|
40
|
+
readonly description: string;
|
|
41
|
+
readonly shortDescription: string;
|
|
42
|
+
readonly inputs: Record<string, PineInput>;
|
|
43
|
+
readonly plots: Record<string, string>;
|
|
44
|
+
readonly script: string;
|
|
45
|
+
constructor(definition: PineIndicatorDefinition);
|
|
46
|
+
/** Study type used to create the indicator on a chart. */
|
|
47
|
+
get type(): PineStudyType;
|
|
48
|
+
/** Changes the study type, for example to run a strategy as `StrategyScript`. */
|
|
49
|
+
setType(type: PineStudyType): this;
|
|
50
|
+
/** Finds the input ID for an input ID (`in_0`), its number (`0`), inline name or internal ID. */
|
|
51
|
+
findInput(key: string | number): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Changes an input value, with type and option validation.
|
|
54
|
+
* @param key Input ID (`in_0`), number (`0`), inline name or internal ID.
|
|
55
|
+
*/
|
|
56
|
+
setInput(key: string | number, value: unknown): this;
|
|
57
|
+
/** Changes several inputs at once. */
|
|
58
|
+
setInputs(values: Record<string, unknown>): this;
|
|
59
|
+
/** Independent copy, so one definition can run with different inputs. */
|
|
60
|
+
clone(): PineIndicator;
|
|
61
|
+
/** Parameters sent with `create_study` / `modify_study`. */
|
|
62
|
+
toStudyInputs(): Record<string, unknown>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { TradingViewError } from '../errors.js';
|
|
2
|
+
const INPUT_TYPES = {
|
|
3
|
+
bool: 'boolean',
|
|
4
|
+
integer: 'number',
|
|
5
|
+
float: 'number',
|
|
6
|
+
text: 'string',
|
|
7
|
+
};
|
|
8
|
+
/** A Pine script (built-in, public, invite-only or private) ready to run on a chart. */
|
|
9
|
+
export class PineIndicator {
|
|
10
|
+
id;
|
|
11
|
+
version;
|
|
12
|
+
description;
|
|
13
|
+
shortDescription;
|
|
14
|
+
inputs;
|
|
15
|
+
plots;
|
|
16
|
+
script;
|
|
17
|
+
#type;
|
|
18
|
+
constructor(definition) {
|
|
19
|
+
this.id = definition.id;
|
|
20
|
+
this.version = definition.version;
|
|
21
|
+
this.description = definition.description;
|
|
22
|
+
this.shortDescription = definition.shortDescription;
|
|
23
|
+
this.inputs = structuredClone(definition.inputs);
|
|
24
|
+
this.plots = { ...definition.plots };
|
|
25
|
+
this.script = definition.script;
|
|
26
|
+
this.#type = definition.type ?? 'Script@tv-scripting-101!';
|
|
27
|
+
}
|
|
28
|
+
/** Study type used to create the indicator on a chart. */
|
|
29
|
+
get type() {
|
|
30
|
+
return this.#type;
|
|
31
|
+
}
|
|
32
|
+
/** Changes the study type, for example to run a strategy as `StrategyScript`. */
|
|
33
|
+
setType(type) {
|
|
34
|
+
this.#type = type;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
/** Finds the input ID for an input ID (`in_0`), its number (`0`), inline name or internal ID. */
|
|
38
|
+
findInput(key) {
|
|
39
|
+
if (this.inputs[`in_${key}`])
|
|
40
|
+
return `in_${key}`;
|
|
41
|
+
if (this.inputs[key])
|
|
42
|
+
return String(key);
|
|
43
|
+
return Object.keys(this.inputs).find((id) => (this.inputs[id].inline === key || this.inputs[id].internalID === key));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Changes an input value, with type and option validation.
|
|
47
|
+
* @param key Input ID (`in_0`), number (`0`), inline name or internal ID.
|
|
48
|
+
*/
|
|
49
|
+
setInput(key, value) {
|
|
50
|
+
const id = this.findInput(key);
|
|
51
|
+
if (!id)
|
|
52
|
+
throw new TradingViewError('INVALID_ARGUMENT', `Input '${key}' not found`);
|
|
53
|
+
const input = this.inputs[id];
|
|
54
|
+
const expected = INPUT_TYPES[input.type];
|
|
55
|
+
if (expected && typeof value !== expected) {
|
|
56
|
+
throw new TradingViewError('INVALID_ARGUMENT', `Input '${input.name}' (${id}) must be a ${expected}`);
|
|
57
|
+
}
|
|
58
|
+
if (input.options && !input.options.includes(value)) {
|
|
59
|
+
throw new TradingViewError('INVALID_ARGUMENT', `Input '${input.name}' (${id}) must be one of: ${input.options.map(String).join(', ')}`, { details: input.options });
|
|
60
|
+
}
|
|
61
|
+
input.value = value;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/** Changes several inputs at once. */
|
|
65
|
+
setInputs(values) {
|
|
66
|
+
for (const [key, value] of Object.entries(values))
|
|
67
|
+
this.setInput(key, value);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
/** Independent copy, so one definition can run with different inputs. */
|
|
71
|
+
clone() {
|
|
72
|
+
return new PineIndicator({
|
|
73
|
+
id: this.id,
|
|
74
|
+
version: this.version,
|
|
75
|
+
description: this.description,
|
|
76
|
+
shortDescription: this.shortDescription,
|
|
77
|
+
inputs: this.inputs,
|
|
78
|
+
plots: this.plots,
|
|
79
|
+
script: this.script,
|
|
80
|
+
type: this.#type,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Parameters sent with `create_study` / `modify_study`. */
|
|
84
|
+
toStudyInputs() {
|
|
85
|
+
const inputs = { text: this.script };
|
|
86
|
+
if (this.id)
|
|
87
|
+
inputs.pineId = this.id;
|
|
88
|
+
if (this.version)
|
|
89
|
+
inputs.pineVersion = this.version;
|
|
90
|
+
Object.keys(this.inputs).forEach((id, index) => {
|
|
91
|
+
const input = this.inputs[id];
|
|
92
|
+
inputs[id] = {
|
|
93
|
+
// TradingView expects colour inputs as their position, not their value.
|
|
94
|
+
v: input.type !== 'color' ? input.value : index,
|
|
95
|
+
f: input.isFake,
|
|
96
|
+
t: input.type,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
return inputs;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TradingView sends large strategy reports as base64 data (`dataCompressed`).
|
|
3
|
+
* The payload is usually a ZIP archive with a single entry, but zlib, raw
|
|
4
|
+
* deflate, gzip and plain JSON have also been observed. Base64 may be URL-safe
|
|
5
|
+
* and unpadded.
|
|
6
|
+
*/
|
|
7
|
+
/** Normalises URL-safe and unpadded base64. */
|
|
8
|
+
export declare function normaliseBase64(data: string): string;
|
|
9
|
+
/** Reads the first file of a ZIP archive (stored or deflated). */
|
|
10
|
+
export declare function readFirstZipEntry(archive: Uint8Array): Uint8Array;
|
|
11
|
+
/** Decodes a TradingView compressed payload into JSON. */
|
|
12
|
+
export declare function decodeCompressed(data: string): unknown;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { gunzipSync, inflateRawSync, inflateSync } from 'node:zlib';
|
|
2
|
+
import { TradingViewError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* TradingView sends large strategy reports as base64 data (`dataCompressed`).
|
|
5
|
+
* The payload is usually a ZIP archive with a single entry, but zlib, raw
|
|
6
|
+
* deflate, gzip and plain JSON have also been observed. Base64 may be URL-safe
|
|
7
|
+
* and unpadded.
|
|
8
|
+
*/
|
|
9
|
+
/** Normalises URL-safe and unpadded base64. */
|
|
10
|
+
export function normaliseBase64(data) {
|
|
11
|
+
const normalised = data.replace(/-/g, '+').replace(/_/g, '/').replace(/\s/g, '');
|
|
12
|
+
return normalised.padEnd(normalised.length + ((4 - (normalised.length % 4)) % 4), '=');
|
|
13
|
+
}
|
|
14
|
+
const LOCAL_FILE_HEADER = 0x04034b50;
|
|
15
|
+
const CENTRAL_DIRECTORY_HEADER = 0x02014b50;
|
|
16
|
+
const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
|
|
17
|
+
function inflateEntry(method, data) {
|
|
18
|
+
if (method === 0)
|
|
19
|
+
return data;
|
|
20
|
+
if (method === 8)
|
|
21
|
+
return inflateRawSync(data);
|
|
22
|
+
throw new Error(`Unsupported ZIP compression method ${method}`);
|
|
23
|
+
}
|
|
24
|
+
/** Reads the first file of a ZIP archive (stored or deflated). */
|
|
25
|
+
export function readFirstZipEntry(archive) {
|
|
26
|
+
const buffer = Buffer.from(archive.buffer, archive.byteOffset, archive.byteLength);
|
|
27
|
+
// Prefer the central directory: local headers may omit sizes (data descriptors).
|
|
28
|
+
for (let eocd = buffer.length - 22; eocd >= Math.max(0, buffer.length - 0xffff - 22); eocd -= 1) {
|
|
29
|
+
if (buffer.readUInt32LE(eocd) !== END_OF_CENTRAL_DIRECTORY)
|
|
30
|
+
continue;
|
|
31
|
+
const entries = buffer.readUInt16LE(eocd + 10);
|
|
32
|
+
let cursor = buffer.readUInt32LE(eocd + 16);
|
|
33
|
+
for (let i = 0; i < entries; i += 1) {
|
|
34
|
+
if (buffer.readUInt32LE(cursor) !== CENTRAL_DIRECTORY_HEADER)
|
|
35
|
+
break;
|
|
36
|
+
const method = buffer.readUInt16LE(cursor + 10);
|
|
37
|
+
const compressedSize = buffer.readUInt32LE(cursor + 20);
|
|
38
|
+
const nameLength = buffer.readUInt16LE(cursor + 28);
|
|
39
|
+
const extraLength = buffer.readUInt16LE(cursor + 30);
|
|
40
|
+
const commentLength = buffer.readUInt16LE(cursor + 32);
|
|
41
|
+
const localOffset = buffer.readUInt32LE(cursor + 42);
|
|
42
|
+
const name = buffer.toString('utf8', cursor + 46, cursor + 46 + nameLength);
|
|
43
|
+
cursor += 46 + nameLength + extraLength + commentLength;
|
|
44
|
+
if (name.endsWith('/'))
|
|
45
|
+
continue; // Directory entry.
|
|
46
|
+
const localNameLength = buffer.readUInt16LE(localOffset + 26);
|
|
47
|
+
const localExtraLength = buffer.readUInt16LE(localOffset + 28);
|
|
48
|
+
const start = localOffset + 30 + localNameLength + localExtraLength;
|
|
49
|
+
return inflateEntry(method, buffer.subarray(start, start + compressedSize));
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
// No usable central directory: fall back to the first local header.
|
|
54
|
+
if (buffer.length >= 30 && buffer.readUInt32LE(0) === LOCAL_FILE_HEADER) {
|
|
55
|
+
const method = buffer.readUInt16LE(8);
|
|
56
|
+
const compressedSize = buffer.readUInt32LE(18);
|
|
57
|
+
const start = 30 + buffer.readUInt16LE(26) + buffer.readUInt16LE(28);
|
|
58
|
+
const end = compressedSize > 0 ? start + compressedSize : buffer.length;
|
|
59
|
+
return inflateEntry(method, buffer.subarray(start, end));
|
|
60
|
+
}
|
|
61
|
+
throw new Error('Not a ZIP archive');
|
|
62
|
+
}
|
|
63
|
+
/** Decodes a TradingView compressed payload into JSON. */
|
|
64
|
+
export function decodeCompressed(data) {
|
|
65
|
+
const buffer = Buffer.from(normaliseBase64(data), 'base64');
|
|
66
|
+
const readers = [
|
|
67
|
+
() => readFirstZipEntry(buffer),
|
|
68
|
+
() => buffer,
|
|
69
|
+
() => inflateSync(buffer),
|
|
70
|
+
() => inflateRawSync(buffer),
|
|
71
|
+
() => gunzipSync(buffer),
|
|
72
|
+
];
|
|
73
|
+
let lastError;
|
|
74
|
+
for (const read of readers) {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(Buffer.from(read()).toString('utf8'));
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
throw new TradingViewError('PARSE_ERROR', 'Unable to decode compressed payload', { cause: lastError });
|
|
83
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TradingView websocket framing.
|
|
3
|
+
*
|
|
4
|
+
* Every websocket message contains one or more frames: `~m~<length>~m~<payload>`.
|
|
5
|
+
* The length counts UTF-16 code units (JavaScript string length). Payloads are
|
|
6
|
+
* either JSON packets or heartbeats (`~h~<id>`), which the client must echo.
|
|
7
|
+
*/
|
|
8
|
+
/** A JSON packet sent by the server: `m` is the method, `p` its parameters. */
|
|
9
|
+
export interface ServerPacket {
|
|
10
|
+
m: string;
|
|
11
|
+
p: unknown[];
|
|
12
|
+
/** Server timestamp in seconds. */
|
|
13
|
+
t?: number;
|
|
14
|
+
/** Server timestamp in milliseconds. */
|
|
15
|
+
t_ms?: number;
|
|
16
|
+
}
|
|
17
|
+
/** Greeting sent by the server right after the websocket opens. */
|
|
18
|
+
export interface ServerHello {
|
|
19
|
+
session_id: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
timestampMs: number;
|
|
22
|
+
release: string;
|
|
23
|
+
studies_metadata_hash: string;
|
|
24
|
+
auth_scheme_vsn: number;
|
|
25
|
+
protocol: 'json' | string;
|
|
26
|
+
via: string;
|
|
27
|
+
javastudies: string[];
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
export type Frame =
|
|
31
|
+
/** Heartbeat to echo back with `encodeHeartbeat(id)`. */
|
|
32
|
+
{
|
|
33
|
+
type: 'heartbeat';
|
|
34
|
+
id: number;
|
|
35
|
+
}
|
|
36
|
+
/** Method packet (`{ m, p }`). */
|
|
37
|
+
| {
|
|
38
|
+
type: 'packet';
|
|
39
|
+
packet: ServerPacket;
|
|
40
|
+
}
|
|
41
|
+
/** Any other JSON payload (for example the server greeting). */
|
|
42
|
+
| {
|
|
43
|
+
type: 'data';
|
|
44
|
+
data: unknown;
|
|
45
|
+
}
|
|
46
|
+
/** A payload that could not be parsed. */
|
|
47
|
+
| {
|
|
48
|
+
type: 'invalid';
|
|
49
|
+
raw: string;
|
|
50
|
+
error: unknown;
|
|
51
|
+
};
|
|
52
|
+
/** Decodes a raw websocket message into frames. */
|
|
53
|
+
export declare function decodeFrames(input: string): Frame[];
|
|
54
|
+
/** Encodes one frame. Objects are JSON-encoded. */
|
|
55
|
+
export declare function encodeFrame(payload: string | object): string;
|
|
56
|
+
/** Encodes a client packet (`{ m: method, p: params }`). */
|
|
57
|
+
export declare function encodePacket(method: string, params?: readonly unknown[]): string;
|
|
58
|
+
/** Encodes the reply to a server heartbeat. */
|
|
59
|
+
export declare function encodeHeartbeat(id: number): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TradingView websocket framing.
|
|
3
|
+
*
|
|
4
|
+
* Every websocket message contains one or more frames: `~m~<length>~m~<payload>`.
|
|
5
|
+
* The length counts UTF-16 code units (JavaScript string length). Payloads are
|
|
6
|
+
* either JSON packets or heartbeats (`~h~<id>`), which the client must echo.
|
|
7
|
+
*/
|
|
8
|
+
const HEADER = '~m~';
|
|
9
|
+
const HEARTBEAT = '~h~';
|
|
10
|
+
function decodePayload(raw) {
|
|
11
|
+
if (raw.startsWith(HEARTBEAT)) {
|
|
12
|
+
const id = Number(raw.slice(HEARTBEAT.length));
|
|
13
|
+
if (Number.isFinite(id))
|
|
14
|
+
return { type: 'heartbeat', id };
|
|
15
|
+
return { type: 'invalid', raw, error: new Error('Invalid heartbeat') };
|
|
16
|
+
}
|
|
17
|
+
let data;
|
|
18
|
+
try {
|
|
19
|
+
data = JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return { type: 'invalid', raw, error };
|
|
23
|
+
}
|
|
24
|
+
if (data && typeof data === 'object' && typeof data.m === 'string') {
|
|
25
|
+
const packet = data;
|
|
26
|
+
return { type: 'packet', packet: { ...packet, p: Array.isArray(packet.p) ? packet.p : [] } };
|
|
27
|
+
}
|
|
28
|
+
return { type: 'data', data };
|
|
29
|
+
}
|
|
30
|
+
/** Lenient fallback for malformed input: split on frame headers. */
|
|
31
|
+
function splitFrames(input) {
|
|
32
|
+
return input.split(/~m~\d+~m~/).filter((part) => part.length > 0);
|
|
33
|
+
}
|
|
34
|
+
/** Decodes a raw websocket message into frames. */
|
|
35
|
+
export function decodeFrames(input) {
|
|
36
|
+
const payloads = [];
|
|
37
|
+
let position = 0;
|
|
38
|
+
while (position < input.length) {
|
|
39
|
+
if (!input.startsWith(HEADER, position)) {
|
|
40
|
+
payloads.push(...splitFrames(input.slice(position)));
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const lengthStart = position + HEADER.length;
|
|
44
|
+
const lengthEnd = input.indexOf(HEADER, lengthStart);
|
|
45
|
+
const length = Number(input.slice(lengthStart, lengthEnd));
|
|
46
|
+
if (lengthEnd < 0 || !Number.isInteger(length) || length < 0) {
|
|
47
|
+
payloads.push(...splitFrames(input.slice(position)));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
const start = lengthEnd + HEADER.length;
|
|
51
|
+
payloads.push(input.slice(start, start + length));
|
|
52
|
+
position = start + length;
|
|
53
|
+
}
|
|
54
|
+
return payloads.map(decodePayload);
|
|
55
|
+
}
|
|
56
|
+
/** Encodes one frame. Objects are JSON-encoded. */
|
|
57
|
+
export function encodeFrame(payload) {
|
|
58
|
+
const body = typeof payload === 'string' ? payload : JSON.stringify(payload);
|
|
59
|
+
return `${HEADER}${body.length}${HEADER}${body}`;
|
|
60
|
+
}
|
|
61
|
+
/** Encodes a client packet (`{ m: method, p: params }`). */
|
|
62
|
+
export function encodePacket(method, params = []) {
|
|
63
|
+
return encodeFrame({ m: method, p: params });
|
|
64
|
+
}
|
|
65
|
+
/** Encodes the reply to a server heartbeat. */
|
|
66
|
+
export function encodeHeartbeat(id) {
|
|
67
|
+
return encodeFrame(`${HEARTBEAT}${id}`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
2
|
+
/** Creates a random session or request ID such as `cs_Ab12Cd34Ef56`. */
|
|
3
|
+
export function createSessionId(prefix = 'xs') {
|
|
4
|
+
const bytes = new Uint8Array(12);
|
|
5
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
6
|
+
let id = '';
|
|
7
|
+
for (const byte of bytes)
|
|
8
|
+
id += ALPHABET[byte % ALPHABET.length];
|
|
9
|
+
return `${prefix}_${id}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { decodeFrames, encodeFrame, encodeHeartbeat, encodePacket, } from './framing.js';
|
|
2
|
+
export type { Frame, ServerHello, ServerPacket } from './framing.js';
|
|
3
|
+
export { decodeCompressed, normaliseBase64, readFirstZipEntry } from './compression.js';
|
|
4
|
+
export { createSessionId } from './ids.js';
|